Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0986812
add border radius to mention user
war-in Aug 12, 2025
195ad24
mention-user highlighting v2
war-in Aug 13, 2025
1b26ad4
mention-user highlighting v3 - it finally works!
war-in Aug 13, 2025
521e78c
rewrite code to enable new mentions in blockquotes & add border radiu…
war-in Aug 14, 2025
9d04008
fix web styles test
war-in Aug 14, 2025
5a7c73c
rename attribute & use custom objects instead of dict
war-in Aug 19, 2025
ddeba51
address review
war-in Aug 19, 2025
ac410ee
support rounded background on android
war-in Aug 20, 2025
870b5da
move `RCTMarkdownTextBackground` & `RCTMarkdownTextBackgroundWithRang…
war-in Aug 20, 2025
99cf1fb
rename cornerRadius to borderRadius
war-in Aug 20, 2025
8b416a5
rename cornerRadius to borderRadius iOS
war-in Aug 20, 2025
28b6a13
fix blockquote `\n` issue
war-in Aug 22, 2025
98dafda
use StaticLayout to correctly calculate text position
war-in Aug 26, 2025
a105c10
create layout for the specific part of the text to improve performance
war-in Aug 26, 2025
db307b2
Merge branch 'main' into war-in/add-mention-user-border-radius
war-in Sep 2, 2025
8d5e9b2
Merge branch 'refs/heads/main' into war-in/add-mention-user-border-ra…
war-in Oct 7, 2025
bd8aaf3
Merge branch 'refs/heads/main' into war-in/add-mention-user-border-ra…
war-in Oct 22, 2025
2a5069e
feat: add support for rounded corners in singleline input on iOS
war-in Oct 22, 2025
cbab252
fix: rounded background issues on singeline input when mentions were …
war-in Oct 27, 2025
f1b8c74
fix: Android - wrap mentions tightly, don't highlight entire line height
war-in Oct 28, 2025
46ad23f
fix: iOS - wrap mentions tightly, don't highlight entire line height
war-in Oct 28, 2025
fe24512
fix: iOS - highlighting multiline mentions
war-in Oct 28, 2025
39e0318
fix: Android - apply density to borderRadius to align radius on all d…
war-in Oct 29, 2025
0da8207
chore: improve the iOS algorithm
war-in Oct 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions WebExample/__tests__/styles.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}) => {
Expand Down
51 changes: 0 additions & 51 deletions apple/BlockquoteTextLayoutFragment.mm

This file was deleted.

2 changes: 2 additions & 0 deletions apple/MarkdownFormatter.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const NSAttributedStringKey RCTLiveMarkdownTextAttributeName = @"RCTLiveMarkdown

const NSAttributedStringKey RCTLiveMarkdownBlockquoteDepthAttributeName = @"RCTLiveMarkdownBlockquoteDepth";

const NSAttributedStringKey RCTLiveMarkdownMentionAttributeName = @"RCTLiveMarkdownMention";

@interface MarkdownFormatter : NSObject

- (void)formatAttributedString:(nonnull NSMutableAttributedString *)attributedString
Expand Down
27 changes: 24 additions & 3 deletions apple/MarkdownFormatter.mm
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,35 @@ - (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, *)) {
[attributedString addAttribute:RCTLiveMarkdownMentionAttributeName
value:@{@"backgroundColor": markdownStyle.mentionHereBackgroundColor,
@"cornerRadius": @(markdownStyle.mentionHereBorderRadius)}
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, *)) {
[attributedString addAttribute:RCTLiveMarkdownMentionAttributeName
value:@{@"backgroundColor": markdownStyle.mentionUserBackgroundColor,
@"cornerRadius": @(markdownStyle.mentionUserBorderRadius)}
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, *)) {
[attributedString addAttribute:RCTLiveMarkdownMentionAttributeName
value:@{@"backgroundColor": markdownStyle.mentionReportBackgroundColor,
@"cornerRadius": @(markdownStyle.mentionReportBorderRadius)}
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];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@
NS_ASSUME_NONNULL_BEGIN

API_AVAILABLE(ios(15.0))
@interface BlockquoteTextLayoutFragment : NSTextLayoutFragment
@interface MarkdownTextLayoutFragment : NSTextLayoutFragment

@property (nonnull, atomic) RCTMarkdownUtils *markdownUtils;

@property NSUInteger depth;
@property (nonnull, atomic) NSNumber* depth;

@end

Expand Down
145 changes: 145 additions & 0 deletions apple/MarkdownTextLayoutFragment.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
#import <RNLiveMarkdown/MarkdownTextLayoutFragment.h>
#import <RNLiveMarkdown/MarkdownFormatter.h>

@implementation MarkdownTextLayoutFragment

#pragma mark - overriding class methods

- (CGRect)renderingSurfaceBounds {
if (self.depth == nil) {
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 drawRibbon];
[self drawMentions];

[super drawAtPoint:point inContext:ctx];
}

#pragma mark - drawing custom elements

- (void)drawRibbon {
if (self.depth == nil) {
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 i = 0; i < [_depth unsignedIntValue]; ++i) {
CGRect ribbonRect = CGRectMake(boundingRect.origin.x + i * shift, boundingRect.origin.y, borderWidth, boundingRect.size.height);
UIRectFill(ribbonRect);
}
}

- (void)drawMentions {
NSMutableArray<NSDictionary *> *mentions = [self getMentions];

[self.textLineFragments enumerateObjectsUsingBlock:^(NSTextLineFragment * _Nonnull lineFragment, NSUInteger idx, BOOL * _Nonnull stop) {
if (lineFragment.characterRange.length == 0) {
return;
}

CGRect lineBounds = lineFragment.typographicBounds;
for (NSDictionary *mention in mentions) {
NSRange mentionRange = [mention[@"range"] rangeValue];
UIColor *backgroundColor = mention[@"value"][@"backgroundColor"];
CGFloat cornerRadius = [mention[@"value"][@"cornerRadius"] floatValue];

NSRange intersection = NSIntersectionRange(lineFragment.characterRange, mentionRange);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to use UITextRange * instead of NSRange if possible

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, both lineFragment.characterRange and mentionRange are NSRanges so we would have to convert them, which I think is not worth the struggle

if (intersection.length == 0) {
continue;
}

BOOL isStart = (intersection.location == mentionRange.location);
BOOL isEnd = (NSMaxRange(intersection) == NSMaxRange(mentionRange));

CGPoint startLocation = [lineFragment locationForCharacterAtIndex:intersection.location];
CGPoint endLocation = [lineFragment locationForCharacterAtIndex:intersection.location + intersection.length];

CGRect paddedRect = CGRectMake(startLocation.x,
lineBounds.origin.y,
endLocation.x - startLocation.x,
lineBounds.size.height);

UIRectCorner cornersToRound = 0;
if (isStart && isEnd) {
cornersToRound = UIRectCornerAllCorners;
} else if (isStart) {
cornersToRound = (UIRectCornerTopLeft | UIRectCornerBottomLeft);
} else if (isEnd) {
cornersToRound = (UIRectCornerTopRight | UIRectCornerBottomRight);
}

UIBezierPath *linePath;
linePath = [UIBezierPath bezierPathWithRoundedRect:paddedRect
byRoundingCorners:cornersToRound
cornerRadii:CGSizeMake(cornerRadius, cornerRadius)];

[backgroundColor 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 unsignedIntValue] - 1);
fragmentTextBounds.size.width = borderWidth + shift * ([_depth unsignedIntValue] - 1);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the blockquote logic is now duplicated, can we somehow dedupe it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was already :/
I could create a helper function to get the shift to make it look better but the logic would stay unchanged

I think we need it to be that way because boundingRect is also used in renderingSurfaceBounds


return fragmentTextBounds;
}

- (NSMutableArray<NSDictionary *>*)getMentions {
NSTextParagraph *paragraph = (NSTextParagraph *)self.textElement;
NSAttributedString *attributedString = [paragraph attributedString];

NSMutableArray<NSDictionary *> *mentions = [NSMutableArray array];
[attributedString enumerateAttribute:RCTLiveMarkdownMentionAttributeName
inRange:NSMakeRange(0, attributedString.length)
options:0
usingBlock:^(id value, NSRange range, BOOL *stop) {
if (value) {
[mentions addObject:@{
@"range": [NSValue valueWithRange:range],
@"value": value
}];
}
}];

return mentions;
}

@end
28 changes: 24 additions & 4 deletions apple/MarkdownTextLayoutManagerDelegate.mm
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#import <RNLiveMarkdown/MarkdownTextLayoutManagerDelegate.h>
#import <RNLiveMarkdown/BlockquoteTextLayoutFragment.h>
#import <RNLiveMarkdown/MarkdownTextLayoutFragment.h>
#import <RNLiveMarkdown/MarkdownFormatter.h>

@implementation MarkdownTextLayoutManagerDelegate
Expand All @@ -9,14 +9,34 @@ - (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];
BOOL hasMention = [self hasMention:textElement];

if (depth != nil || hasMention) {
MarkdownTextLayoutFragment *textLayoutFragment = [[MarkdownTextLayoutFragment alloc] initWithTextElement:textElement range:textElement.elementRange];
textLayoutFragment.markdownUtils = _markdownUtils;
textLayoutFragment.depth = [depth unsignedIntValue];
textLayoutFragment.depth = depth;
return textLayoutFragment;
}
}
return [[NSTextLayoutFragment alloc] initWithTextElement:textElement range:textElement.elementRange];
}

- (BOOL)hasMention:(NSTextElement *)textElement {
NSTextParagraph *paragraph = (NSTextParagraph *)textElement;
NSAttributedString *attributedString = [paragraph attributedString];

__block BOOL hasMention = NO;
[attributedString enumerateAttribute:RCTLiveMarkdownMentionAttributeName
inRange:NSMakeRange(0, attributedString.length)
options:0
usingBlock:^(id value, NSRange range, BOOL *stop) {
if (value) {
hasMention = YES;
*stop = YES;
}
}];

return hasMention;
}

@end
3 changes: 3 additions & 0 deletions apple/RCTMarkdownStyle.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
3 changes: 3 additions & 0 deletions apple/RCTMarkdownStyle.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions src/styleUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,17 @@ function makeDefaultMarkdownStyle(): MarkdownStyle {
mentionHere: {
color: 'green',
backgroundColor: 'lime',
borderRadius: 5.0,
},
mentionUser: {
color: 'blue',
backgroundColor: 'cyan',
borderRadius: 5.0,
},
mentionReport: {
color: 'red',
backgroundColor: 'pink',
borderRadius: 5.0,
},
inlineImage: {
minWidth: 50,
Expand Down
Loading