diff --git a/WebExample/__tests__/styles.spec.ts b/WebExample/__tests__/styles.spec.ts index b33db80cf..9f87b4631 100644 --- a/WebExample/__tests__/styles.spec.ts +++ b/WebExample/__tests__/styles.spec.ts @@ -40,15 +40,15 @@ test.describe('markdown content styling', () => { }); test('mention-here', async ({page}) => { - await testMarkdownContentStyle({testContent: 'here', style: 'color: green; background-color: lime;', page}); + await testMarkdownContentStyle({testContent: 'here', style: 'color: green; background-color: lime; border-radius: 5px;', page}); }); test('mention-user', async ({page}) => { - await testMarkdownContentStyle({testContent: 'someone@swmansion.com', style: 'color: blue; background-color: cyan;', page}); + await testMarkdownContentStyle({testContent: 'someone@swmansion.com', style: 'color: blue; background-color: cyan; border-radius: 5px;', page}); }); test('mention-report', async ({page}) => { - await testMarkdownContentStyle({testContent: 'mention-report', style: 'color: red; background-color: pink;', page}); + await testMarkdownContentStyle({testContent: 'mention-report', style: 'color: red; background-color: pink; border-radius: 5px;', page}); }); test('blockquote', async ({page, browserName}) => { diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java index 78b554cdb..ffd3220da 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java @@ -75,16 +75,16 @@ private void applyRange(@NonNull SpannableStringBuilder ssb, @NonNull MarkdownRa break; case "mention-here": setSpan(ssb, new MarkdownForegroundColorSpan(markdownStyle.getMentionHereColor()), start, end); - setSpan(ssb, new MarkdownBackgroundColorSpan(markdownStyle.getMentionHereBackgroundColor()), start, end); + setSpan(ssb, new MarkdownBackgroundSpan(markdownStyle.getMentionHereBackgroundColor(), markdownStyle.getMentionHereBorderRadius(), start, end), start, end); break; case "mention-user": // TODO: change mention color when it mentions current user setSpan(ssb, new MarkdownForegroundColorSpan(markdownStyle.getMentionUserColor()), start, end); - setSpan(ssb, new MarkdownBackgroundColorSpan(markdownStyle.getMentionUserBackgroundColor()), start, end); + setSpan(ssb, new MarkdownBackgroundSpan(markdownStyle.getMentionUserBackgroundColor(), markdownStyle.getMentionUserBorderRadius(), start, end), start, end); break; case "mention-report": setSpan(ssb, new MarkdownForegroundColorSpan(markdownStyle.getMentionReportColor()), start, end); - setSpan(ssb, new MarkdownBackgroundColorSpan(markdownStyle.getMentionReportBackgroundColor()), start, end); + setSpan(ssb, new MarkdownBackgroundSpan(markdownStyle.getMentionReportBackgroundColor(), markdownStyle.getMentionReportBorderRadius(), start, end), start, end); break; case "syntax": setSpan(ssb, new MarkdownForegroundColorSpan(markdownStyle.getSyntaxColor()), start, end); diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownStyle.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownStyle.java index ca71e51cd..34e9c6320 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownStyle.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownStyle.java @@ -63,19 +63,27 @@ public class MarkdownStyle { @ColorInt private final int mMentionHereBackgroundColor; + private final float mMentionHereBorderRadius; + @ColorInt private final int mMentionUserColor; @ColorInt private final int mMentionUserBackgroundColor; + private final float mMentionUserBorderRadius; + @ColorInt private final int mMentionReportColor; @ColorInt private final int mMentionReportBackgroundColor; + private final float mMentionReportBorderRadius; + public MarkdownStyle(@NonNull ReadableMap map, @NonNull Context context) { + float screenDensity = context.getResources().getDisplayMetrics().density; + mSyntaxColor = parseColor(map, "syntax", "color", context); mLinkColor = parseColor(map, "link", "color", context); mH1FontSize = parseFloat(map, "h1", "fontSize"); @@ -95,10 +103,13 @@ public MarkdownStyle(@NonNull ReadableMap map, @NonNull Context context) { mPreBackgroundColor = parseColor(map, "pre", "backgroundColor", context); mMentionHereColor = parseColor(map, "mentionHere", "color", context); mMentionHereBackgroundColor = parseColor(map, "mentionHere", "backgroundColor", context); + mMentionHereBorderRadius = parseFloat(map, "mentionHere", "borderRadius") * screenDensity; mMentionUserColor = parseColor(map, "mentionUser", "color", context); mMentionUserBackgroundColor = parseColor(map, "mentionUser", "backgroundColor", context); + mMentionUserBorderRadius = parseFloat(map, "mentionUser", "borderRadius") * screenDensity; mMentionReportColor = parseColor(map, "mentionReport", "color", context); mMentionReportBackgroundColor = parseColor(map, "mentionReport", "backgroundColor", context); + mMentionReportBorderRadius = parseFloat(map, "mentionReport", "borderRadius") * screenDensity; } private static int parseColor(@NonNull ReadableMap map, @NonNull String key, @NonNull String prop, @NonNull Context context) { @@ -213,6 +224,10 @@ public int getMentionHereBackgroundColor() { return mMentionHereBackgroundColor; } + public float getMentionHereBorderRadius() { + return mMentionHereBorderRadius; + } + @ColorInt public int getMentionUserColor() { return mMentionUserColor; @@ -223,6 +238,10 @@ public int getMentionUserBackgroundColor() { return mMentionUserBackgroundColor; } + public float getMentionUserBorderRadius() { + return mMentionUserBorderRadius; + } + @ColorInt public int getMentionReportColor() { return mMentionReportColor; @@ -232,4 +251,8 @@ public int getMentionReportColor() { public int getMentionReportBackgroundColor() { return mMentionReportBackgroundColor; } + + public float getMentionReportBorderRadius() { + return mMentionReportBorderRadius; + } } diff --git a/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownBackgroundSpan.java b/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownBackgroundSpan.java new file mode 100644 index 000000000..bccd714bb --- /dev/null +++ b/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownBackgroundSpan.java @@ -0,0 +1,94 @@ +package com.expensify.livemarkdown.spans; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.text.style.LineBackgroundSpan; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; + +public class MarkdownBackgroundSpan implements MarkdownSpan, LineBackgroundSpan { + + private final int backgroundColor; + private final int mentionStart; + private final int mentionEnd; + private final float borderRadius; + + private StaticLayout layout; + private Path backgroundPath; + + public MarkdownBackgroundSpan(@ColorInt int backgroundColor, float borderRadius, int mentionStart, int mentionEnd) { + this.backgroundColor = backgroundColor; + this.borderRadius = borderRadius; + this.mentionStart = mentionStart; + this.mentionEnd = mentionEnd; + this.backgroundPath = new Path(); + } + + @Override + public void drawBackground( + @NonNull Canvas canvas, + @NonNull Paint paint, + int left, + int right, + int top, + int baseline, + int bottom, + @NonNull CharSequence text, + int start, + int end, + int lnum + ) { + int lineStart = 0; + int lineEnd = end - start; + CharSequence lineText = text.subSequence(start, end); + if (layout == null || layout.getText() != lineText || layout.getWidth() != right || layout.getLineEnd(0) != lineEnd) { + // Create layout for the current line only + layout = StaticLayout.Builder.obtain(lineText, lineStart, lineEnd, (TextPaint) paint, right).build(); + + int relativeMentionStart = mentionStart - start; + int relativeMentionEnd = mentionEnd - start; + + boolean mentionStarts = lineStart <= relativeMentionStart; + boolean mentionEnds = lineEnd >= relativeMentionEnd; + + float startX = layout.getPrimaryHorizontal(mentionStarts ? relativeMentionStart: lineStart); + float endX = layout.getPrimaryHorizontal(mentionEnds ? relativeMentionEnd : lineEnd); + + Paint.FontMetrics fm = paint.getFontMetrics(); + float startY = baseline + fm.ascent; + float endY = baseline + fm.descent; + + RectF lineRect = new RectF(startX, startY, endX, endY); + backgroundPath.reset(); + backgroundPath.addRoundRect(lineRect, createRadii(mentionStarts, mentionEnds), Path.Direction.CW); + } + + int originalColor = paint.getColor(); + paint.setColor(backgroundColor); + + canvas.drawPath(backgroundPath, paint); + + paint.setColor(originalColor); + } + + private float[] createRadii(boolean roundedLeft, boolean roundedRight) { + float[] radii = new float[8]; + + if (roundedLeft) { + radii[0] = radii[1] = borderRadius; // top-left + radii[6] = radii[7] = borderRadius; // bottom-left + } + + if (roundedRight) { + radii[2] = radii[3] = borderRadius; // top-right + radii[4] = radii[5] = borderRadius; // bottom-right + } + + return radii; + } +} diff --git a/apple/BlockquoteTextLayoutFragment.mm b/apple/BlockquoteTextLayoutFragment.mm deleted file mode 100644 index 78396d317..000000000 --- a/apple/BlockquoteTextLayoutFragment.mm +++ /dev/null @@ -1,51 +0,0 @@ -#import - -@implementation BlockquoteTextLayoutFragment - -- (CGRect)boundingRect { - CGRect fragmentTextBounds = CGRectNull; - for (NSTextLineFragment *lineFragment in self.textLineFragments) { - if (lineFragment.characterRange.length == 0) { - continue; - } - CGRect lineFragmentBounds = lineFragment.typographicBounds; - if (CGRectIsNull(fragmentTextBounds)) { - fragmentTextBounds = lineFragmentBounds; - } else { - fragmentTextBounds = CGRectUnion(fragmentTextBounds, lineFragmentBounds); - } - } - - CGFloat marginLeft = _markdownUtils.markdownStyle.blockquoteMarginLeft; - CGFloat borderWidth = _markdownUtils.markdownStyle.blockquoteBorderWidth; - CGFloat paddingLeft = _markdownUtils.markdownStyle.blockquotePaddingLeft; - CGFloat shift = marginLeft + borderWidth + paddingLeft; - - fragmentTextBounds.origin.x -= (paddingLeft + borderWidth) + shift * (_depth - 1); - fragmentTextBounds.size.width = borderWidth + shift * (_depth - 1); - - return fragmentTextBounds; -} - -- (void)drawAtPoint:(CGPoint)point inContext:(CGContextRef)ctx { - CGFloat marginLeft = _markdownUtils.markdownStyle.blockquoteMarginLeft; - CGFloat borderWidth = _markdownUtils.markdownStyle.blockquoteBorderWidth; - CGFloat paddingLeft = _markdownUtils.markdownStyle.blockquotePaddingLeft; - CGFloat shift = marginLeft + borderWidth + paddingLeft; - - [_markdownUtils.markdownStyle.blockquoteBorderColor setFill]; - - CGRect boundingRect = self.boundingRect; - for (NSUInteger i = 0; i < _depth; ++i) { - CGRect ribbonRect = CGRectMake(boundingRect.origin.x + i * shift, boundingRect.origin.y, borderWidth, boundingRect.size.height); - UIRectFill(ribbonRect); - } - - [super drawAtPoint:point inContext:ctx]; -} - -- (CGRect)renderingSurfaceBounds { - return CGRectUnion(self.boundingRect, [super renderingSurfaceBounds]); -} - -@end diff --git a/apple/MarkdownFormatter.h b/apple/MarkdownFormatter.h index e583e2741..aad961ee6 100644 --- a/apple/MarkdownFormatter.h +++ b/apple/MarkdownFormatter.h @@ -6,6 +6,8 @@ NS_ASSUME_NONNULL_BEGIN const NSAttributedStringKey RCTLiveMarkdownTextAttributeName = @"RCTLiveMarkdownText"; +const NSAttributedStringKey RCTLiveMarkdownTextBackgroundAttributeName = @"RCTLiveMarkdownTextBackground"; + const NSAttributedStringKey RCTLiveMarkdownBlockquoteDepthAttributeName = @"RCTLiveMarkdownBlockquoteDepth"; @interface MarkdownFormatter : NSObject diff --git a/apple/MarkdownFormatter.mm b/apple/MarkdownFormatter.mm index b47c4ac74..cf94500c2 100644 --- a/apple/MarkdownFormatter.mm +++ b/apple/MarkdownFormatter.mm @@ -1,5 +1,6 @@ #import "MarkdownFormatter.h" #import +#import @implementation MarkdownFormatter @@ -91,14 +92,44 @@ - (void)applyRangeToAttributedString:(NSMutableAttributedString *)attributedStri [attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.codeBackgroundColor range:range]; } else if (type == "mention-here") { [attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.mentionHereColor range:range]; - [attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.mentionHereBackgroundColor range:range]; + if (@available(iOS 16.0, *)) { + RCTMarkdownTextBackground *textBackground = [[RCTMarkdownTextBackground alloc] init]; + textBackground.color = markdownStyle.mentionHereBackgroundColor; + textBackground.borderRadius = markdownStyle.mentionHereBorderRadius; + + [attributedString addAttribute:RCTLiveMarkdownTextBackgroundAttributeName + value:textBackground + range:range]; + } else { + [attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.mentionHereBackgroundColor range:range]; + } } else if (type == "mention-user") { // TODO: change mention color when it mentions current user [attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.mentionUserColor range:range]; - [attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.mentionUserBackgroundColor range:range]; + if (@available(iOS 16.0, *)) { + RCTMarkdownTextBackground *textBackground = [[RCTMarkdownTextBackground alloc] init]; + textBackground.color = markdownStyle.mentionUserBackgroundColor; + textBackground.borderRadius = markdownStyle.mentionUserBorderRadius; + + [attributedString addAttribute:RCTLiveMarkdownTextBackgroundAttributeName + value:textBackground + range:range]; + } else { + [attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.mentionUserBackgroundColor range:range]; + } } else if (type == "mention-report") { [attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.mentionReportColor range:range]; - [attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.mentionReportBackgroundColor range:range]; + if (@available(iOS 16.0, *)) { + RCTMarkdownTextBackground *textBackground = [[RCTMarkdownTextBackground alloc] init]; + textBackground.color = markdownStyle.mentionReportBackgroundColor; + textBackground.borderRadius = markdownStyle.mentionReportBorderRadius; + + [attributedString addAttribute:RCTLiveMarkdownTextBackgroundAttributeName + value:textBackground + range:range]; + } else { + [attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.mentionReportBackgroundColor range:range]; + } } else if (type == "link") { [attributedString addAttribute:NSUnderlineStyleAttributeName value:[NSNumber numberWithInteger:NSUnderlineStyleSingle] range:range]; [attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.linkColor range:range]; diff --git a/apple/MarkdownTextInputDecoratorComponentView.mm b/apple/MarkdownTextInputDecoratorComponentView.mm index 912c262ac..52d19e964 100644 --- a/apple/MarkdownTextInputDecoratorComponentView.mm +++ b/apple/MarkdownTextInputDecoratorComponentView.mm @@ -77,7 +77,7 @@ - (void)addTextInputObservers react_native_assert([childView isKindOfClass:[RCTTextInputComponentView class]] && "Child component of MarkdownTextInputDecoratorComponentView is not an instance of RCTTextInputComponentView."); RCTTextInputComponentView *textInputComponentView = (RCTTextInputComponentView *)childView; UIView *backedTextInputView = [textInputComponentView valueForKey:@"_backedTextInputView"]; - + _observersAdded = true; if ([backedTextInputView isKindOfClass:[RCTUITextField class]]) { @@ -100,6 +100,17 @@ - (void)addTextInputObservers // format initial value [_markdownTextFieldObserver textFieldDidChange:_textField]; + if (@available(iOS 16.0, *)) { + NSTextStorage *textStorage = [_textField valueForKey:@"_textStorage"]; + NSTextContainer *textContainer = [_textField valueForKey:@"_textContainer"]; + NSTextLayoutManager *textLayoutManager = [textContainer valueForKey:@"_textLayoutManager"]; + + _markdownTextLayoutManagerDelegate = [[MarkdownTextLayoutManagerDelegate alloc] init]; + _markdownTextLayoutManagerDelegate.textStorage = textStorage; + _markdownTextLayoutManagerDelegate.markdownUtils = _markdownUtils; + textLayoutManager.delegate = _markdownTextLayoutManagerDelegate; + } + // TODO: register blockquotes layout manager // https://github.com/Expensify/react-native-live-markdown/issues/87 } else if ([backedTextInputView isKindOfClass:[RCTUITextView class]]) { @@ -229,7 +240,7 @@ - (void)prepareForRecycle { react_native_assert(!_observersAdded && "MarkdownTextInputDecoratorComponentView was being recycled with TextInput observers still attached"); [super prepareForRecycle]; - + static const auto defaultProps = std::make_shared(); _props = defaultProps; _markdownUtils = [[RCTMarkdownUtils alloc] init]; diff --git a/apple/BlockquoteTextLayoutFragment.h b/apple/MarkdownTextLayoutFragment.h similarity index 55% rename from apple/BlockquoteTextLayoutFragment.h rename to apple/MarkdownTextLayoutFragment.h index cdabf6fd0..7edcf4f03 100644 --- a/apple/BlockquoteTextLayoutFragment.h +++ b/apple/MarkdownTextLayoutFragment.h @@ -1,15 +1,18 @@ #import +#import #import NS_ASSUME_NONNULL_BEGIN API_AVAILABLE(ios(15.0)) -@interface BlockquoteTextLayoutFragment : NSTextLayoutFragment +@interface MarkdownTextLayoutFragment : NSTextLayoutFragment @property (nonnull, atomic) RCTMarkdownUtils *markdownUtils; @property NSUInteger depth; +@property NSMutableArray *mentions; + @end NS_ASSUME_NONNULL_END diff --git a/apple/MarkdownTextLayoutFragment.mm b/apple/MarkdownTextLayoutFragment.mm new file mode 100644 index 000000000..702e9fbcb --- /dev/null +++ b/apple/MarkdownTextLayoutFragment.mm @@ -0,0 +1,159 @@ +#import +#import + +@implementation MarkdownTextLayoutFragment + +#pragma mark - overriding class methods + +- (CGRect)renderingSurfaceBounds { + if (self.depth == 0) { + return [super renderingSurfaceBounds]; + } + return CGRectUnion(self.boundingRect, [super renderingSurfaceBounds]); +} + +- (void)drawAtPoint:(CGPoint)point inContext:(CGContextRef)ctx { + if (self.textLineFragments.count == 0) { + [super drawAtPoint:point inContext:ctx]; + return; + } + + [self drawBlockquoteRibbons]; + [self drawMentions]; + + [super drawAtPoint:point inContext:ctx]; +} + +#pragma mark - drawing custom elements + +- (void)drawBlockquoteRibbons { + if (self.depth == 0) { + return; + } + + CGFloat marginLeft = _markdownUtils.markdownStyle.blockquoteMarginLeft; + CGFloat borderWidth = _markdownUtils.markdownStyle.blockquoteBorderWidth; + CGFloat paddingLeft = _markdownUtils.markdownStyle.blockquotePaddingLeft; + CGFloat shift = marginLeft + borderWidth + paddingLeft; + + [_markdownUtils.markdownStyle.blockquoteBorderColor setFill]; + + CGRect boundingRect = self.boundingRect; + for (NSUInteger level = 0; level < _depth; ++level) { + CGFloat x = boundingRect.origin.x + level * shift; + CGRect ribbonRect = CGRectMake(x, boundingRect.origin.y, borderWidth, boundingRect.size.height); + UIRectFill(ribbonRect); + } +} + +- (void)drawMentions { + if (self.mentions.count == 0) { + return; + } + + bool isSingleline = [self.textLineFragments count] == 1; + __block NSUInteger mentionIndex = 0; + [self.textLineFragments enumerateObjectsUsingBlock:^(NSTextLineFragment * _Nonnull lineFragment, NSUInteger idx, BOOL * _Nonnull stop) { + if (lineFragment.characterRange.length == 0) { + return; + } + + CGRect lineBounds = lineFragment.typographicBounds; + NSRange lineRange = lineFragment.characterRange; + CGPoint lineEndLocation = [lineFragment locationForCharacterAtIndex: lineFragment.characterRange.length]; + + while (mentionIndex < self.mentions.count && + NSMaxRange(self.mentions[mentionIndex].range) <= lineRange.location) { + mentionIndex++; + } + + for (NSUInteger i = mentionIndex; i < self.mentions.count; i++) { + RCTMarkdownTextBackgroundWithRange *mention = self.mentions[i]; + NSRange mentionRange = mention.range; + + if (mentionRange.location >= NSMaxRange(lineRange)) { + break; + } + + NSRange intersection = NSIntersectionRange(lineRange, mentionRange); + CGPoint startLocation = [lineFragment locationForCharacterAtIndex:intersection.location]; + if (isSingleline && startLocation.x == 0 && intersection.location > 0) { + // singleline: mention starts off screen, no need to draw background + continue; + } + + CGPoint endLocation = [lineFragment locationForCharacterAtIndex:intersection.location + intersection.length]; + if (isSingleline && (startLocation.x > endLocation.x || (startLocation.x == endLocation.x && intersection.location == 0))) { + // singleline: mention is partially visible + // 1. starts in the middle, or + // 2. starts at the beginning of the line + endLocation = lineEndLocation; + } + + CGFloat width = endLocation.x - startLocation.x; + CGFloat x = lineBounds.origin.x + startLocation.x; + + UIFont *font = [lineFragment.attributedString attribute:NSFontAttributeName + atIndex:intersection.location + effectiveRange:NULL]; + CGFloat ascent = font.ascender; + CGFloat textHeight = font.lineHeight; + CGFloat y = lineBounds.origin.y + startLocation.y - ascent; + + + CGRect backgroundRect = CGRectMake(x, + y, + width, + textHeight); + + BOOL isStart = (intersection.location == mention.range.location); + BOOL isEnd = (NSMaxRange(intersection) == NSMaxRange(mention.range)); + UIRectCorner cornersToRound = 0; + if (isStart) { + cornersToRound |= UIRectCornerTopLeft | UIRectCornerBottomLeft; + } + if (isEnd) { + cornersToRound |= UIRectCornerTopRight | UIRectCornerBottomRight; + } + + UIBezierPath *linePath = (cornersToRound == 0) + ? [UIBezierPath bezierPathWithRect:backgroundRect] + : [UIBezierPath bezierPathWithRoundedRect:backgroundRect + byRoundingCorners:cornersToRound + cornerRadii:CGSizeMake(mention.textBackground.borderRadius, + mention.textBackground.borderRadius)]; + + [mention.textBackground.color setFill]; + [linePath fill]; + } + }]; +} + +#pragma mark - helper functions + +- (CGRect)boundingRect { + CGRect fragmentTextBounds = CGRectNull; + for (NSTextLineFragment *lineFragment in self.textLineFragments) { + if (lineFragment.characterRange.length == 0) { + continue; + } + CGRect lineFragmentBounds = lineFragment.typographicBounds; + if (CGRectIsNull(fragmentTextBounds)) { + fragmentTextBounds = lineFragmentBounds; + } else { + fragmentTextBounds = CGRectUnion(fragmentTextBounds, lineFragmentBounds); + } + } + + CGFloat marginLeft = _markdownUtils.markdownStyle.blockquoteMarginLeft; + CGFloat borderWidth = _markdownUtils.markdownStyle.blockquoteBorderWidth; + CGFloat paddingLeft = _markdownUtils.markdownStyle.blockquotePaddingLeft; + CGFloat shift = marginLeft + borderWidth + paddingLeft; + + fragmentTextBounds.origin.x -= (paddingLeft + borderWidth) + shift * (_depth - 1); + fragmentTextBounds.size.width = borderWidth + shift * (_depth - 1); + + return fragmentTextBounds; +} + +@end diff --git a/apple/MarkdownTextLayoutManagerDelegate.mm b/apple/MarkdownTextLayoutManagerDelegate.mm index 55ce6e107..924318c2b 100644 --- a/apple/MarkdownTextLayoutManagerDelegate.mm +++ b/apple/MarkdownTextLayoutManagerDelegate.mm @@ -1,6 +1,7 @@ #import -#import +#import #import +#import @implementation MarkdownTextLayoutManagerDelegate @@ -9,10 +10,27 @@ - (NSTextLayoutFragment *)textLayoutManager:(NSTextLayoutManager *)textLayoutMan NSInteger index = [textLayoutManager offsetFromLocation:textLayoutManager.documentRange.location toLocation:location]; if (index < self.textStorage.length) { NSNumber *depth = [self.textStorage attribute:RCTLiveMarkdownBlockquoteDepthAttributeName atIndex:index effectiveRange:nil]; - if (depth != nil) { - BlockquoteTextLayoutFragment *textLayoutFragment = [[BlockquoteTextLayoutFragment alloc] initWithTextElement:textElement range:textElement.elementRange]; + + NSAttributedString *attributedString = [(NSTextParagraph *)textElement attributedString]; + NSMutableArray *mentions = [NSMutableArray array]; + [attributedString enumerateAttribute:RCTLiveMarkdownTextBackgroundAttributeName + inRange:NSMakeRange(0, attributedString.length) + options:0 + usingBlock:^(id value, NSRange range, BOOL *stop) { + if (value) { + RCTMarkdownTextBackgroundWithRange *textBackgroundWithRange = [[RCTMarkdownTextBackgroundWithRange alloc] init]; + textBackgroundWithRange.textBackground = value; + textBackgroundWithRange.range = range; + + [mentions addObject:textBackgroundWithRange]; + } + }]; + + if (depth != nil || mentions.count > 0) { + MarkdownTextLayoutFragment *textLayoutFragment = [[MarkdownTextLayoutFragment alloc] initWithTextElement:textElement range:textElement.elementRange]; textLayoutFragment.markdownUtils = _markdownUtils; - textLayoutFragment.depth = [depth unsignedIntValue]; + textLayoutFragment.depth = depth != nil ? [depth unsignedIntValue] : 0; + textLayoutFragment.mentions = mentions; return textLayoutFragment; } } diff --git a/apple/RCTMarkdownStyle.h b/apple/RCTMarkdownStyle.h index fd88fcafa..f57fa41ae 100644 --- a/apple/RCTMarkdownStyle.h +++ b/apple/RCTMarkdownStyle.h @@ -25,10 +25,13 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) UIColor *preBackgroundColor; @property (nonatomic) UIColor *mentionHereColor; @property (nonatomic) UIColor *mentionHereBackgroundColor; +@property (nonatomic) CGFloat mentionHereBorderRadius; @property (nonatomic) UIColor *mentionUserColor; @property (nonatomic) UIColor *mentionUserBackgroundColor; +@property (nonatomic) CGFloat mentionUserBorderRadius; @property (nonatomic) UIColor *mentionReportColor; @property (nonatomic) UIColor *mentionReportBackgroundColor; +@property (nonatomic) CGFloat mentionReportBorderRadius; - (instancetype)initWithStruct:(const facebook::react::MarkdownTextInputDecoratorViewMarkdownStyleStruct &)style; diff --git a/apple/RCTMarkdownStyle.mm b/apple/RCTMarkdownStyle.mm index f56f01ae2..4598d945c 100644 --- a/apple/RCTMarkdownStyle.mm +++ b/apple/RCTMarkdownStyle.mm @@ -33,12 +33,15 @@ - (instancetype)initWithStruct:(const facebook::react::MarkdownTextInputDecorato _mentionHereColor = RCTUIColorFromSharedColor(style.mentionHere.color); _mentionHereBackgroundColor = RCTUIColorFromSharedColor(style.mentionHere.backgroundColor); + _mentionHereBorderRadius = style.mentionHere.borderRadius; _mentionUserColor = RCTUIColorFromSharedColor(style.mentionUser.color); _mentionUserBackgroundColor = RCTUIColorFromSharedColor(style.mentionUser.backgroundColor); + _mentionUserBorderRadius = style.mentionUser.borderRadius; _mentionReportColor = RCTUIColorFromSharedColor(style.mentionReport.color); _mentionReportBackgroundColor = RCTUIColorFromSharedColor(style.mentionReport.backgroundColor); + _mentionReportBorderRadius = style.mentionReport.borderRadius; } return self; diff --git a/apple/RCTMarkdownTextBackgroundUtils.h b/apple/RCTMarkdownTextBackgroundUtils.h new file mode 100644 index 000000000..369d6cfdb --- /dev/null +++ b/apple/RCTMarkdownTextBackgroundUtils.h @@ -0,0 +1,18 @@ +#import + +@interface RCTMarkdownTextBackground : NSObject + +@property (nonatomic, strong) UIColor *color; + +@property (nonatomic, assign) CGFloat borderRadius; + +@end + + +@interface RCTMarkdownTextBackgroundWithRange : NSObject + +@property (nonnull, atomic) RCTMarkdownTextBackground *textBackground; + +@property NSRange range; + +@end diff --git a/apple/RCTMarkdownTextBackgroundUtils.mm b/apple/RCTMarkdownTextBackgroundUtils.mm new file mode 100644 index 000000000..0dfa78db4 --- /dev/null +++ b/apple/RCTMarkdownTextBackgroundUtils.mm @@ -0,0 +1,7 @@ +#import + +@implementation RCTMarkdownTextBackground +@end + +@implementation RCTMarkdownTextBackgroundWithRange +@end diff --git a/src/styleUtils.ts b/src/styleUtils.ts index 7396dce79..c97fd1e4e 100644 --- a/src/styleUtils.ts +++ b/src/styleUtils.ts @@ -62,14 +62,17 @@ function makeDefaultMarkdownStyle(): MarkdownStyle { mentionHere: { color: 'green', backgroundColor: 'lime', + borderRadius: 5, }, mentionUser: { color: 'blue', backgroundColor: 'cyan', + borderRadius: 5, }, mentionReport: { color: 'red', backgroundColor: 'pink', + borderRadius: 5, }, inlineImage: { minWidth: 50,