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,83 @@
1
+ #import "ENRMChipDrawing.h"
2
+
3
+ static UIBezierPath *chipFillPath(CGRect rect, CGFloat r, BOOL isFirst, BOOL isLast)
4
+ {
5
+ #if !TARGET_OS_OSX
6
+ UIRectCorner corners = 0;
7
+ if (isFirst)
8
+ corners |= (UIRectCornerTopLeft | UIRectCornerBottomLeft);
9
+ if (isLast)
10
+ corners |= (UIRectCornerTopRight | UIRectCornerBottomRight);
11
+
12
+ return [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:corners cornerRadii:CGSizeMake(r, r)];
13
+ #else
14
+ // NSBezierPath doesn't support per-corner rounding — use uniform corner radius
15
+ return UIBezierPathWithRoundedRect(rect, (isFirst || isLast) ? r : 0);
16
+ #endif
17
+ }
18
+
19
+ static UIBezierPath *chipOpenBorderPath(CGRect rect, CGFloat r, CGFloat borderWidth, BOOL isFirst, BOOL isLast)
20
+ {
21
+ UIBezierPath *path = [UIBezierPath bezierPath];
22
+ CGFloat inset = borderWidth / 2.0;
23
+ CGRect insetRect = CGRectInset(rect, inset, inset);
24
+
25
+ if (isFirst) {
26
+ [path moveToPoint:CGPointMake(CGRectGetMaxX(rect), insetRect.origin.y)];
27
+ BezierPathAddLine(path, CGPointMake(insetRect.origin.x + r, insetRect.origin.y));
28
+ BezierPathAddQuadCurve(path, CGPointMake(insetRect.origin.x, insetRect.origin.y + r),
29
+ CGPointMake(insetRect.origin.x, insetRect.origin.y));
30
+ BezierPathAddLine(path, CGPointMake(insetRect.origin.x, CGRectGetMaxY(insetRect) - r));
31
+ BezierPathAddQuadCurve(path, CGPointMake(insetRect.origin.x + r, CGRectGetMaxY(insetRect)),
32
+ CGPointMake(insetRect.origin.x, CGRectGetMaxY(insetRect)));
33
+ BezierPathAddLine(path, CGPointMake(CGRectGetMaxX(rect), CGRectGetMaxY(insetRect)));
34
+ } else if (isLast) {
35
+ [path moveToPoint:CGPointMake(rect.origin.x, insetRect.origin.y)];
36
+ BezierPathAddLine(path, CGPointMake(CGRectGetMaxX(insetRect) - r, insetRect.origin.y));
37
+ BezierPathAddQuadCurve(path, CGPointMake(CGRectGetMaxX(insetRect), insetRect.origin.y + r),
38
+ CGPointMake(CGRectGetMaxX(insetRect), insetRect.origin.y));
39
+ BezierPathAddLine(path, CGPointMake(CGRectGetMaxX(insetRect), CGRectGetMaxY(insetRect) - r));
40
+ BezierPathAddQuadCurve(path, CGPointMake(CGRectGetMaxX(insetRect) - r, CGRectGetMaxY(insetRect)),
41
+ CGPointMake(CGRectGetMaxX(insetRect), CGRectGetMaxY(insetRect)));
42
+ BezierPathAddLine(path, CGPointMake(rect.origin.x, CGRectGetMaxY(insetRect)));
43
+ } else {
44
+ [path moveToPoint:CGPointMake(rect.origin.x, insetRect.origin.y)];
45
+ BezierPathAddLine(path, CGPointMake(CGRectGetMaxX(rect), insetRect.origin.y));
46
+ [path moveToPoint:CGPointMake(rect.origin.x, CGRectGetMaxY(insetRect))];
47
+ BezierPathAddLine(path, CGPointMake(CGRectGetMaxX(rect), CGRectGetMaxY(insetRect)));
48
+ }
49
+ return path;
50
+ }
51
+
52
+ void ENRMDrawChipFragment(CGRect rect, RCTUIColor *backgroundColor, RCTUIColor *borderColor, CGFloat borderWidth,
53
+ CGFloat cornerRadius, BOOL isFirst, BOOL isLast, BOOL strokeInset)
54
+ {
55
+ if (CGRectIsEmpty(rect))
56
+ return;
57
+
58
+ CGFloat r = MIN(MAX(cornerRadius, 0), MIN(rect.size.width, rect.size.height) / 2.0);
59
+
60
+ if (backgroundColor) {
61
+ [backgroundColor setFill];
62
+ [chipFillPath(rect, r, isFirst, isLast) fill];
63
+ }
64
+
65
+ if (!borderColor || borderWidth <= 0)
66
+ return;
67
+
68
+ [borderColor setStroke];
69
+ UIBezierPath *strokePath;
70
+ if (isFirst && isLast) {
71
+ if (strokeInset) {
72
+ CGFloat inset = borderWidth / 2.0;
73
+ strokePath = chipFillPath(CGRectInset(rect, inset, inset), MAX(r - inset, 0), YES, YES);
74
+ } else {
75
+ strokePath = chipFillPath(rect, r, YES, YES);
76
+ }
77
+ } else {
78
+ strokePath = chipOpenBorderPath(rect, r, borderWidth, isFirst, isLast);
79
+ }
80
+ strokePath.lineWidth = borderWidth;
81
+ BezierPathSetRoundStyle(strokePath);
82
+ [strokePath stroke];
83
+ }
@@ -0,0 +1,70 @@
1
+ #pragma once
2
+ #import "ENRMUIKit.h"
3
+
4
+ NS_ASSUME_NONNULL_BEGIN
5
+
6
+ /// Marks a link range that carries chip styling. The value is one
7
+ /// `ENRMLinkChip` instance per link: instances compare by identity, so two
8
+ /// adjacent links never coalesce into one attribute run (one chip).
9
+ extern NSString *const ENRMLinkChipAttributeName;
10
+
11
+ /// Marks the spacer characters `LinkRenderer` inserts at both outer edges of a
12
+ /// link to reserve `paddingHorizontal` in the line. Spacers are layout-only:
13
+ /// every text exit point (copy, markdown/HTML export, decoration text) strips
14
+ /// them by this attribute — never by character, since U+00A0 can be content.
15
+ extern NSString *const ENRMLinkSpacerAttributeName;
16
+
17
+ @interface ENRMLinkChip : NSObject
18
+ @property (nonatomic, strong, nullable) RCTUIColor *backgroundColor;
19
+ @property (nonatomic, strong, nullable) RCTUIColor *borderColor;
20
+ @property (nonatomic, assign) CGFloat borderWidth;
21
+ @property (nonatomic, assign) CGFloat borderRadius;
22
+ @property (nonatomic, assign) CGFloat paddingHorizontal;
23
+ @property (nonatomic, assign) CGFloat paddingVertical;
24
+ /// YES when the chip paints something (a background or a visible border).
25
+ @property (nonatomic, readonly) BOOL isVisible;
26
+ @end
27
+
28
+ #ifdef __cplusplus
29
+ extern "C" {
30
+ #endif
31
+
32
+ /// A spacer character (U+00A0) whose advance is exactly `width`.
33
+ ///
34
+ /// U+00A0 is line-break class GL: a break is allowed before it after a space
35
+ /// but never after it, and never before it after a non-space — so a leading
36
+ /// spacer always travels with the link's first glyph and a trailing one with
37
+ /// its last glyph (no orphaned padding at a wrap). It uses a 1pt system font
38
+ /// so its own advance and line-height contribution are negligible whatever
39
+ /// the link font (a custom font may lack U+00A0), and NSKern makes up the rest.
40
+ NSAttributedString *ENRMLinkSpacerString(NSDictionary<NSAttributedStringKey, id> *baseAttributes, CGFloat width);
41
+
42
+ /// `string` without link spacers (returns `string` itself when it has none).
43
+ NSAttributedString *ENRMAttributedStringByRemovingLinkSpacers(NSAttributedString *string);
44
+
45
+ /// Plain text of `range` without link spacers.
46
+ NSString *ENRMPlainTextByRemovingLinkSpacers(NSAttributedString *string, NSRange range);
47
+
48
+ /// Falls back to a plain `NSBackgroundColorAttributeName` for every chip
49
+ /// background, for text drawn without `TextViewLayoutManager` (table cells,
50
+ /// RTF export). Radius, border and vertical padding are not represented.
51
+ void ENRMApplyLinkChipBackgroundsAsTextBackground(NSMutableAttributedString *string);
52
+
53
+ /// Enumerates the chip rect of every laid-out line fragment of the link at
54
+ /// `charRange`, in text-container coordinates. The rect spans the glyphs
55
+ /// (spacers included, so it already covers `paddingHorizontal`) horizontally
56
+ /// and the link font's ascender..descender ± `paddingVertical` vertically.
57
+ void ENRMEnumerateLinkChipFragments(NSLayoutManager *layoutManager, NSTextContainer *textContainer, NSRange charRange,
58
+ ENRMLinkChip *_Nullable chip,
59
+ void (^block)(CGRect rect, BOOL isFirst, BOOL isLast));
60
+
61
+ /// Draws the visible link chips intersecting `glyphsToShow`. Called from the
62
+ /// layout manager's background pass, before glyphs are drawn.
63
+ void ENRMDrawLinkChipsForGlyphRange(NSLayoutManager *layoutManager, NSTextContainer *textContainer,
64
+ NSRange glyphsToShow, CGPoint origin);
65
+
66
+ #ifdef __cplusplus
67
+ }
68
+ #endif
69
+
70
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,213 @@
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
+ NSRange fragmentChars = [layoutManager characterRangeForGlyphRange:intersect
138
+ actualGlyphRange:NULL];
139
+ NSUInteger lineEnd = NSMaxRange(lineRange);
140
+ CGFloat left =
141
+ lineRect.origin.x + [layoutManager locationForGlyphAtIndex:intersect.location].x;
142
+
143
+ NSUInteger visibleCharEnd = NSMaxRange(fragmentChars);
144
+ NSString *chars = storage.string;
145
+ NSCharacterSet *breakable = [NSCharacterSet whitespaceAndNewlineCharacterSet];
146
+ while (visibleCharEnd > fragmentChars.location &&
147
+ [chars characterAtIndex:visibleCharEnd - 1] != kSpacerChar &&
148
+ [breakable characterIsMember:[chars characterAtIndex:visibleCharEnd - 1]]) {
149
+ visibleCharEnd--;
150
+ }
151
+ NSUInteger visibleGlyphEnd = NSMaxRange(intersect);
152
+ if (visibleCharEnd < NSMaxRange(fragmentChars)) {
153
+ visibleGlyphEnd = [layoutManager glyphIndexForCharacterAtIndex:visibleCharEnd];
154
+ }
155
+
156
+ CGFloat right;
157
+ if (visibleGlyphEnd < lineEnd) {
158
+ right =
159
+ lineRect.origin.x + [layoutManager locationForGlyphAtIndex:visibleGlyphEnd].x;
160
+ } else {
161
+ right = CGRectGetMaxX(usedRect);
162
+ }
163
+ if (isLast && paddingHorizontal > 0) {
164
+ NSUInteger lastGlyph = NSMaxRange(intersect) - 1;
165
+ CGFloat spacerX =
166
+ lineRect.origin.x + [layoutManager locationForGlyphAtIndex:lastGlyph].x;
167
+ right = MAX(right, spacerX + paddingHorizontal);
168
+ }
169
+
170
+ UIFont *font = chipFontInRange(storage, fragmentChars);
171
+ CGFloat top = CGRectGetMinY(textRect);
172
+ CGFloat bottom = CGRectGetMaxY(textRect);
173
+ if (font) {
174
+ // locationForGlyphAtIndex: y is the baseline within the line fragment.
175
+ CGFloat baseline =
176
+ lineRect.origin.y + [layoutManager locationForGlyphAtIndex:intersect.location].y;
177
+ top = baseline - font.ascender;
178
+ bottom = baseline - font.descender;
179
+ }
180
+ top -= paddingVertical;
181
+ bottom += paddingVertical;
182
+
183
+ block(CGRectMake(left, top, right - left, bottom - top), isFirst, isLast);
184
+ }];
185
+ }
186
+
187
+ #pragma mark - Drawing
188
+
189
+ void ENRMDrawLinkChipsForGlyphRange(NSLayoutManager *layoutManager, NSTextContainer *textContainer,
190
+ NSRange glyphsToShow, CGPoint origin)
191
+ {
192
+ NSTextStorage *storage = layoutManager.textStorage;
193
+ NSRange charRange = [layoutManager characterRangeForGlyphRange:glyphsToShow actualGlyphRange:NULL];
194
+ if (charRange.location == NSNotFound || charRange.length == 0)
195
+ return;
196
+
197
+ // Enumerate the whole storage (like CodeBackground) so a chip that starts
198
+ // before the dirty range still gets its full rect.
199
+ [storage
200
+ enumerateAttribute:ENRMLinkChipAttributeName
201
+ inRange:NSMakeRange(0, storage.length)
202
+ options:0
203
+ usingBlock:^(ENRMLinkChip *chip, NSRange range, BOOL *stop) {
204
+ if (!chip.isVisible || NSIntersectionRange(range, charRange).length == 0)
205
+ return;
206
+
207
+ ENRMEnumerateLinkChipFragments(
208
+ layoutManager, textContainer, range, chip, ^(CGRect rect, BOOL isFirst, BOOL isLast) {
209
+ ENRMDrawChipFragment(CGRectOffset(rect, origin.x, origin.y), chip.backgroundColor,
210
+ chip.borderColor, chip.borderWidth, chip.borderRadius, isFirst, isLast, YES);
211
+ });
212
+ }];
213
+ }
@@ -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,120 @@
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
+ extern NSString *const ENRMSelectionAdjustingKey;
18
+ /// Only in `ENRMTextSelectionInTextView` results: whether the range spans the
19
+ /// whole text (same threshold `markdownForRange` uses for the cached markdown).
20
+ extern NSString *const ENRMSelectionFullKey;
21
+
22
+ #ifdef __cplusplus
23
+ extern "C" {
24
+ #endif
25
+
26
+ /// The payload reported once the selection is gone.
27
+ NSDictionary *ENRMInactiveSelectionPayload(void);
28
+
29
+ /// A long-pressed block (table / math / code): `whole`, every rect is `frame`.
30
+ NSDictionary *ENRMBlockSelectionPayload(NSString *source, NSString *markdown, NSString *text, CGRect frame);
31
+
32
+ #if !TARGET_OS_OSX
33
+ /// Text, full-selection flag and rects of `textView`'s selection, converted to
34
+ /// `targetView`'s coordinates; nil when nothing is selected. `rect` is the union
35
+ /// of the selection rects, `startRect` / `endRect` the rects holding its start
36
+ /// and end. The owner adds source, markdown and whole to build the payload.
37
+ NSDictionary *_Nullable ENRMTextSelectionInTextView(UITextView *textView, UIView *targetView);
38
+ #endif
39
+
40
+ #ifdef __cplusplus
41
+ }
42
+ #endif
43
+
44
+ /// Coalesces selection changes into at most one report per run loop turn. The
45
+ /// payload is read when the turn fires, so the latest state always reaches JS;
46
+ /// a cleared selection is reported right away and drops any pending update.
47
+ /// Identical payloads are sent once.
48
+ @interface ENRMSelectionChangeReporter : NSObject
49
+
50
+ - (instancetype)initWithSnapshot:(NSDictionary * (^)(void))snapshot
51
+ emit:(void (^)(NSDictionary *payload))emit NS_DESIGNATED_INITIALIZER;
52
+ - (instancetype)init NS_UNAVAILABLE;
53
+
54
+ /// Off by default. Toggling resets the last reported payload to inactive.
55
+ @property (nonatomic, assign) BOOL enabled;
56
+
57
+ - (void)schedule;
58
+ - (void)selectionChanged:(BOOL)hasSelection;
59
+ - (void)flushNow;
60
+ - (void)cancel;
61
+ /// Makes the next report go out even if it equals the last one — a block
62
+ /// long-pressed again is a new request for the app's menu, not a repeat.
63
+ - (void)forgetLastReported;
64
+
65
+ @end
66
+
67
+ #if !TARGET_OS_OSX
68
+ /// Tells when the user is dragging a selection handle (or holding the loupe).
69
+ /// `UITextView` doesn't call `textViewDidChangeSelection:` during the drag, so
70
+ /// this watches the text view's own selection gestures: every recognizer of
71
+ /// the view and its subviews except scroll, pinch and taps. UIKit adds the
72
+ /// handle recognizers lazily, so call `trackTextView:` again on each selection
73
+ /// change. `change` runs whenever `adjusting` flips.
74
+ @interface ENRMSelectionGestureTracker : NSObject
75
+
76
+ - (instancetype)initWithChange:(void (^)(void))change NS_DESIGNATED_INITIALIZER;
77
+ - (instancetype)init NS_UNAVAILABLE;
78
+
79
+ /// A tracked gesture is in `Began` / `Changed`.
80
+ @property (nonatomic, readonly) BOOL adjusting;
81
+
82
+ - (void)trackTextView:(UITextView *)textView;
83
+ /// Clears `adjusting` without calling `change` (view recycling).
84
+ - (void)reset;
85
+
86
+ @end
87
+ #endif
88
+
89
+ #ifdef __cplusplus
90
+ /// Converts a payload into a codegen `OnSelectionChange` struct. Templated
91
+ /// because each component has its own (structurally identical) event struct.
92
+ template <typename Event> inline Event ENRMSelectionChangeEventFromPayload(NSDictionary *payload)
93
+ {
94
+ Event event;
95
+ event.active = [payload[ENRMSelectionActiveKey] boolValue];
96
+ event.source = std::string([payload[ENRMSelectionSourceKey] UTF8String] ?: "selection");
97
+ event.text = std::string([payload[ENRMSelectionTextKey] UTF8String] ?: "");
98
+ event.markdown = std::string([payload[ENRMSelectionMarkdownKey] UTF8String] ?: "");
99
+ event.whole = [payload[ENRMSelectionWholeKey] boolValue];
100
+ event.adjusting = [payload[ENRMSelectionAdjustingKey] boolValue];
101
+
102
+ auto assign = [](auto &target, NSValue *value) {
103
+ #if TARGET_OS_OSX
104
+ CGRect rect = value ? NSRectToCGRect(value.rectValue) : CGRectZero;
105
+ #else
106
+ CGRect rect = value ? value.CGRectValue : CGRectZero;
107
+ #endif
108
+ target.x = rect.origin.x;
109
+ target.y = rect.origin.y;
110
+ target.width = rect.size.width;
111
+ target.height = rect.size.height;
112
+ };
113
+ assign(event.rect, payload[ENRMSelectionRectKey]);
114
+ assign(event.startRect, payload[ENRMSelectionStartRectKey]);
115
+ assign(event.endRect, payload[ENRMSelectionEndRectKey]);
116
+ return event;
117
+ }
118
+ #endif
119
+
120
+ NS_ASSUME_NONNULL_END