From 0986812227686fbf0fbaf3239508f9f448d9e07d Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 12 Aug 2025 13:57:36 +0200 Subject: [PATCH 01/24] add border radius to mention user --- apple/MarkdownFormatter.h | 2 + apple/MarkdownFormatter.mm | 2 +- apple/MarkdownTextLayoutManagerDelegate.mm | 8 ++++ apple/MentionBorderLayoutFragment.h | 13 +++++++ apple/MentionBorderLayoutFragment.mm | 45 ++++++++++++++++++++++ 5 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 apple/MentionBorderLayoutFragment.h create mode 100644 apple/MentionBorderLayoutFragment.mm diff --git a/apple/MarkdownFormatter.h b/apple/MarkdownFormatter.h index e583e274..32cb515d 100644 --- a/apple/MarkdownFormatter.h +++ b/apple/MarkdownFormatter.h @@ -8,6 +8,8 @@ const NSAttributedStringKey RCTLiveMarkdownTextAttributeName = @"RCTLiveMarkdown const NSAttributedStringKey RCTLiveMarkdownBlockquoteDepthAttributeName = @"RCTLiveMarkdownBlockquoteDepth"; +const NSAttributedStringKey RCTLiveMarkdownMentionUserAttributeName = @"RCTLiveMarkdownMentionUser"; + @interface MarkdownFormatter : NSObject - (void)formatAttributedString:(nonnull NSMutableAttributedString *)attributedString diff --git a/apple/MarkdownFormatter.mm b/apple/MarkdownFormatter.mm index b47c4ac7..fa3ddd9f 100644 --- a/apple/MarkdownFormatter.mm +++ b/apple/MarkdownFormatter.mm @@ -95,7 +95,7 @@ - (void)applyRangeToAttributedString:(NSMutableAttributedString *)attributedStri } 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]; + [attributedString addAttribute:RCTLiveMarkdownMentionUserAttributeName value:@(YES) range:range]; } else if (type == "mention-report") { [attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.mentionReportColor range:range]; [attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.mentionReportBackgroundColor range:range]; diff --git a/apple/MarkdownTextLayoutManagerDelegate.mm b/apple/MarkdownTextLayoutManagerDelegate.mm index 55ce6e10..d63bcbf2 100644 --- a/apple/MarkdownTextLayoutManagerDelegate.mm +++ b/apple/MarkdownTextLayoutManagerDelegate.mm @@ -1,6 +1,7 @@ #import #import #import +#import @implementation MarkdownTextLayoutManagerDelegate @@ -15,6 +16,13 @@ - (NSTextLayoutFragment *)textLayoutManager:(NSTextLayoutManager *)textLayoutMan textLayoutFragment.depth = [depth unsignedIntValue]; return textLayoutFragment; } + + NSNumber *isMentionUser = [self.textStorage attribute:RCTLiveMarkdownMentionUserAttributeName atIndex:index effectiveRange:nil]; + if (isMentionUser) { + MentionBorderLayoutFragment *textLayoutFragment = [[MentionBorderLayoutFragment alloc] initWithTextElement:textElement range:textElement.elementRange]; + textLayoutFragment.markdownUtils = _markdownUtils; + return textLayoutFragment; + } } return [[NSTextLayoutFragment alloc] initWithTextElement:textElement range:textElement.elementRange]; } diff --git a/apple/MentionBorderLayoutFragment.h b/apple/MentionBorderLayoutFragment.h new file mode 100644 index 00000000..4c1101fe --- /dev/null +++ b/apple/MentionBorderLayoutFragment.h @@ -0,0 +1,13 @@ +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +API_AVAILABLE(ios(15.0)) +@interface MentionBorderLayoutFragment : NSTextLayoutFragment + +@property (nonnull, atomic) RCTMarkdownUtils *markdownUtils; + +@end + +NS_ASSUME_NONNULL_END diff --git a/apple/MentionBorderLayoutFragment.mm b/apple/MentionBorderLayoutFragment.mm new file mode 100644 index 00000000..64532013 --- /dev/null +++ b/apple/MentionBorderLayoutFragment.mm @@ -0,0 +1,45 @@ +#import + +static const CGFloat kCornerRadius = 5.0; + +@implementation MentionBorderLayoutFragment + +- (void)drawAtPoint:(CGPoint)point inContext:(CGContextRef)ctx { + if (self.textLineFragments.count == 0) { + [super drawAtPoint:point inContext:ctx]; + return; + } + + [_markdownUtils.markdownStyle.mentionUserBackgroundColor setFill]; + + UIBezierPath *fullPath = [UIBezierPath new]; + NSUInteger lineCount = self.textLineFragments.count; + + [self.textLineFragments enumerateObjectsUsingBlock:^(NSTextLineFragment * _Nonnull lineFragment, NSUInteger idx, BOOL * _Nonnull stop) { + if (lineFragment.characterRange.length == 0) { + return; + } + + CGRect lineBounds = lineFragment.typographicBounds; + CGRect paddedRect = CGRectInset(lineBounds, 0, 0); + + UIRectCorner cornersToRound = 0; + if (lineCount == 1) { + cornersToRound = UIRectCornerAllCorners; + } else if (idx == 0) { + cornersToRound = UIRectCornerTopLeft | UIRectCornerBottomLeft; + } else if (idx == lineCount - 1) { + cornersToRound = UIRectCornerTopRight | UIRectCornerBottomRight; + } + + UIBezierPath *linePath = [UIBezierPath bezierPathWithRoundedRect:paddedRect + byRoundingCorners:cornersToRound + cornerRadii:CGSizeMake(kCornerRadius, kCornerRadius)]; + [fullPath appendPath:linePath]; + }]; + + [fullPath fill]; + [super drawAtPoint:point inContext:ctx]; +} + +@end From 195ad2476182541102d30e5e355ce30eabf5b270 Mon Sep 17 00:00:00 2001 From: war-in Date: Wed, 13 Aug 2025 17:28:06 +0200 Subject: [PATCH 02/24] mention-user highlighting v2 --- apple/MarkdownTextLayoutManagerDelegate.mm | 9 ++---- apple/MentionBorderLayoutFragment.mm | 33 ++++++++++++++-------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/apple/MarkdownTextLayoutManagerDelegate.mm b/apple/MarkdownTextLayoutManagerDelegate.mm index d63bcbf2..d18cb88e 100644 --- a/apple/MarkdownTextLayoutManagerDelegate.mm +++ b/apple/MarkdownTextLayoutManagerDelegate.mm @@ -17,12 +17,9 @@ - (NSTextLayoutFragment *)textLayoutManager:(NSTextLayoutManager *)textLayoutMan return textLayoutFragment; } - NSNumber *isMentionUser = [self.textStorage attribute:RCTLiveMarkdownMentionUserAttributeName atIndex:index effectiveRange:nil]; - if (isMentionUser) { - MentionBorderLayoutFragment *textLayoutFragment = [[MentionBorderLayoutFragment alloc] initWithTextElement:textElement range:textElement.elementRange]; - textLayoutFragment.markdownUtils = _markdownUtils; - return textLayoutFragment; - } + MentionBorderLayoutFragment *textLayoutFragment = [[MentionBorderLayoutFragment alloc] initWithTextElement:textElement range:textElement.elementRange]; + textLayoutFragment.markdownUtils = _markdownUtils; + return textLayoutFragment; } return [[NSTextLayoutFragment alloc] initWithTextElement:textElement range:textElement.elementRange]; } diff --git a/apple/MentionBorderLayoutFragment.mm b/apple/MentionBorderLayoutFragment.mm index 64532013..da858ea3 100644 --- a/apple/MentionBorderLayoutFragment.mm +++ b/apple/MentionBorderLayoutFragment.mm @@ -1,4 +1,5 @@ #import +#import static const CGFloat kCornerRadius = 5.0; @@ -13,29 +14,39 @@ - (void)drawAtPoint:(CGPoint)point inContext:(CGContextRef)ctx { [_markdownUtils.markdownStyle.mentionUserBackgroundColor setFill]; UIBezierPath *fullPath = [UIBezierPath new]; - NSUInteger lineCount = self.textLineFragments.count; [self.textLineFragments enumerateObjectsUsingBlock:^(NSTextLineFragment * _Nonnull lineFragment, NSUInteger idx, BOOL * _Nonnull stop) { if (lineFragment.characterRange.length == 0) { return; } + NSAttributedString *attributedString = [lineFragment attributedString]; + NSMutableArray *rangesWithAttribute = [NSMutableArray array]; + [attributedString enumerateAttribute:RCTLiveMarkdownMentionUserAttributeName + inRange:lineFragment.characterRange + options:0 + usingBlock:^(id value, NSRange range, BOOL *stop) { + if ([attributedString attribute:RCTLiveMarkdownMentionUserAttributeName atIndex:range.location effectiveRange:nil]) { + [rangesWithAttribute addObject:[NSValue valueWithRange:range]]; + } + }]; + + for (NSValue *rangeValue in rangesWithAttribute) { + NSRange range = [rangeValue rangeValue]; CGRect lineBounds = lineFragment.typographicBounds; - CGRect paddedRect = CGRectInset(lineBounds, 0, 0); - - UIRectCorner cornersToRound = 0; - if (lineCount == 1) { - cornersToRound = UIRectCornerAllCorners; - } else if (idx == 0) { - cornersToRound = UIRectCornerTopLeft | UIRectCornerBottomLeft; - } else if (idx == lineCount - 1) { - cornersToRound = UIRectCornerTopRight | UIRectCornerBottomRight; - } + + CGPoint startLocation = [lineFragment locationForCharacterAtIndex:range.location]; + CGPoint endLocation = [lineFragment locationForCharacterAtIndex:range.location + range.length]; + startLocation.y = idx*lineBounds.size.height; + CGRect paddedRect = CGRect(startLocation, CGSize(endLocation.x-startLocation.x, lineBounds.size.height)); + + UIRectCorner cornersToRound = UIRectCornerAllCorners; UIBezierPath *linePath = [UIBezierPath bezierPathWithRoundedRect:paddedRect byRoundingCorners:cornersToRound cornerRadii:CGSizeMake(kCornerRadius, kCornerRadius)]; [fullPath appendPath:linePath]; + } }]; [fullPath fill]; From 1b26ad4104783cf0113e528b1dd5ca59ae360d98 Mon Sep 17 00:00:00 2001 From: war-in Date: Wed, 13 Aug 2025 18:00:14 +0200 Subject: [PATCH 03/24] mention-user highlighting v3 - it finally works! --- apple/MentionBorderLayoutFragment.mm | 104 ++++++++++++++++----------- 1 file changed, 61 insertions(+), 43 deletions(-) diff --git a/apple/MentionBorderLayoutFragment.mm b/apple/MentionBorderLayoutFragment.mm index da858ea3..30e577ca 100644 --- a/apple/MentionBorderLayoutFragment.mm +++ b/apple/MentionBorderLayoutFragment.mm @@ -6,51 +6,69 @@ @implementation MentionBorderLayoutFragment - (void)drawAtPoint:(CGPoint)point inContext:(CGContextRef)ctx { - if (self.textLineFragments.count == 0) { - [super drawAtPoint:point inContext:ctx]; - return; + if (self.textLineFragments.count == 0) { + [super drawAtPoint:point inContext:ctx]; + return; + } + + NSAttributedString *attributedString = [self.textLineFragments.firstObject attributedString]; + NSMutableArray *mentionRanges = [NSMutableArray array]; + [attributedString enumerateAttribute:RCTLiveMarkdownMentionUserAttributeName + inRange:NSMakeRange(0, attributedString.length) + options:0 + usingBlock:^(id value, NSRange range, BOOL *stop) { + if (value) { + [mentionRanges addObject:[NSValue valueWithRange:range]]; } - - [_markdownUtils.markdownStyle.mentionUserBackgroundColor setFill]; - - UIBezierPath *fullPath = [UIBezierPath new]; - - [self.textLineFragments enumerateObjectsUsingBlock:^(NSTextLineFragment * _Nonnull lineFragment, NSUInteger idx, BOOL * _Nonnull stop) { - if (lineFragment.characterRange.length == 0) { - return; - } - - NSAttributedString *attributedString = [lineFragment attributedString]; - NSMutableArray *rangesWithAttribute = [NSMutableArray array]; - [attributedString enumerateAttribute:RCTLiveMarkdownMentionUserAttributeName - inRange:lineFragment.characterRange - options:0 - usingBlock:^(id value, NSRange range, BOOL *stop) { - if ([attributedString attribute:RCTLiveMarkdownMentionUserAttributeName atIndex:range.location effectiveRange:nil]) { - [rangesWithAttribute addObject:[NSValue valueWithRange:range]]; - } - }]; - - for (NSValue *rangeValue in rangesWithAttribute) { - NSRange range = [rangeValue rangeValue]; - CGRect lineBounds = lineFragment.typographicBounds; - - CGPoint startLocation = [lineFragment locationForCharacterAtIndex:range.location]; - CGPoint endLocation = [lineFragment locationForCharacterAtIndex:range.location + range.length]; - startLocation.y = idx*lineBounds.size.height; - CGRect paddedRect = CGRect(startLocation, CGSize(endLocation.x-startLocation.x, lineBounds.size.height)); - - UIRectCorner cornersToRound = UIRectCornerAllCorners; - - UIBezierPath *linePath = [UIBezierPath bezierPathWithRoundedRect:paddedRect - byRoundingCorners:cornersToRound - cornerRadii:CGSizeMake(kCornerRadius, kCornerRadius)]; - [fullPath appendPath:linePath]; + }]; + + [_markdownUtils.markdownStyle.mentionUserBackgroundColor setFill]; + UIBezierPath *fullPath = [UIBezierPath new]; + + [self.textLineFragments enumerateObjectsUsingBlock:^(NSTextLineFragment * _Nonnull lineFragment, NSUInteger idx, BOOL * _Nonnull stop) { + if (lineFragment.characterRange.length == 0) { + return; + } + + CGRect lineBounds = lineFragment.typographicBounds; + for (NSValue *rangeValue in mentionRanges) { + NSRange mentionRange = [rangeValue rangeValue]; + NSRange intersection = NSIntersectionRange(lineFragment.characterRange, mentionRange); + if (intersection.length == 0) { + continue; } - }]; - - [fullPath fill]; - [super drawAtPoint:point inContext:ctx]; + + 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(kCornerRadius, kCornerRadius)]; + + [fullPath appendPath:linePath]; + } + }]; + + [fullPath fill]; + [super drawAtPoint:point inContext:ctx]; } @end From 521e78c72cd02b3a1083b741bca272336e13fc92 Mon Sep 17 00:00:00 2001 From: war-in Date: Thu, 14 Aug 2025 13:36:03 +0200 Subject: [PATCH 04/24] rewrite code to enable new mentions in blockquotes & add border radius to all mentions --- apple/BlockquoteTextLayoutFragment.mm | 51 ------ apple/MarkdownFormatter.h | 2 +- apple/MarkdownFormatter.mm | 27 +++- ...ragment.h => MarkdownTextLayoutFragment.h} | 5 +- apple/MarkdownTextLayoutFragment.mm | 145 ++++++++++++++++++ apple/MarkdownTextLayoutManagerDelegate.mm | 33 ++-- apple/MentionBorderLayoutFragment.h | 13 -- apple/MentionBorderLayoutFragment.mm | 74 --------- apple/RCTMarkdownStyle.h | 3 + apple/RCTMarkdownStyle.mm | 3 + src/styleUtils.ts | 3 + 11 files changed, 205 insertions(+), 154 deletions(-) delete mode 100644 apple/BlockquoteTextLayoutFragment.mm rename apple/{BlockquoteTextLayoutFragment.h => MarkdownTextLayoutFragment.h} (66%) create mode 100644 apple/MarkdownTextLayoutFragment.mm delete mode 100644 apple/MentionBorderLayoutFragment.h delete mode 100644 apple/MentionBorderLayoutFragment.mm diff --git a/apple/BlockquoteTextLayoutFragment.mm b/apple/BlockquoteTextLayoutFragment.mm deleted file mode 100644 index 78396d31..00000000 --- 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 32cb515d..b27f2e13 100644 --- a/apple/MarkdownFormatter.h +++ b/apple/MarkdownFormatter.h @@ -8,7 +8,7 @@ const NSAttributedStringKey RCTLiveMarkdownTextAttributeName = @"RCTLiveMarkdown const NSAttributedStringKey RCTLiveMarkdownBlockquoteDepthAttributeName = @"RCTLiveMarkdownBlockquoteDepth"; -const NSAttributedStringKey RCTLiveMarkdownMentionUserAttributeName = @"RCTLiveMarkdownMentionUser"; +const NSAttributedStringKey RCTLiveMarkdownMentionAttributeName = @"RCTLiveMarkdownMention"; @interface MarkdownFormatter : NSObject diff --git a/apple/MarkdownFormatter.mm b/apple/MarkdownFormatter.mm index fa3ddd9f..86c8a95f 100644 --- a/apple/MarkdownFormatter.mm +++ b/apple/MarkdownFormatter.mm @@ -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:RCTLiveMarkdownMentionUserAttributeName value:@(YES) 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]; diff --git a/apple/BlockquoteTextLayoutFragment.h b/apple/MarkdownTextLayoutFragment.h similarity index 66% rename from apple/BlockquoteTextLayoutFragment.h rename to apple/MarkdownTextLayoutFragment.h index cdabf6fd..29828564 100644 --- a/apple/BlockquoteTextLayoutFragment.h +++ b/apple/MarkdownTextLayoutFragment.h @@ -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 diff --git a/apple/MarkdownTextLayoutFragment.mm b/apple/MarkdownTextLayoutFragment.mm new file mode 100644 index 00000000..aec077bc --- /dev/null +++ b/apple/MarkdownTextLayoutFragment.mm @@ -0,0 +1,145 @@ +#import +#import + +@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 *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); + 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); + + return fragmentTextBounds; +} + +- (NSMutableArray*)getMentions { + NSTextParagraph *paragraph = (NSTextParagraph *)self.textElement; + NSAttributedString *attributedString = [paragraph attributedString]; + + NSMutableArray *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 diff --git a/apple/MarkdownTextLayoutManagerDelegate.mm b/apple/MarkdownTextLayoutManagerDelegate.mm index d18cb88e..67e0e31b 100644 --- a/apple/MarkdownTextLayoutManagerDelegate.mm +++ b/apple/MarkdownTextLayoutManagerDelegate.mm @@ -1,7 +1,6 @@ #import -#import +#import #import -#import @implementation MarkdownTextLayoutManagerDelegate @@ -10,18 +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; } - - MentionBorderLayoutFragment *textLayoutFragment = [[MentionBorderLayoutFragment alloc] initWithTextElement:textElement range:textElement.elementRange]; - textLayoutFragment.markdownUtils = _markdownUtils; - 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 diff --git a/apple/MentionBorderLayoutFragment.h b/apple/MentionBorderLayoutFragment.h deleted file mode 100644 index 4c1101fe..00000000 --- a/apple/MentionBorderLayoutFragment.h +++ /dev/null @@ -1,13 +0,0 @@ -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -API_AVAILABLE(ios(15.0)) -@interface MentionBorderLayoutFragment : NSTextLayoutFragment - -@property (nonnull, atomic) RCTMarkdownUtils *markdownUtils; - -@end - -NS_ASSUME_NONNULL_END diff --git a/apple/MentionBorderLayoutFragment.mm b/apple/MentionBorderLayoutFragment.mm deleted file mode 100644 index 30e577ca..00000000 --- a/apple/MentionBorderLayoutFragment.mm +++ /dev/null @@ -1,74 +0,0 @@ -#import -#import - -static const CGFloat kCornerRadius = 5.0; - -@implementation MentionBorderLayoutFragment - -- (void)drawAtPoint:(CGPoint)point inContext:(CGContextRef)ctx { - if (self.textLineFragments.count == 0) { - [super drawAtPoint:point inContext:ctx]; - return; - } - - NSAttributedString *attributedString = [self.textLineFragments.firstObject attributedString]; - NSMutableArray *mentionRanges = [NSMutableArray array]; - [attributedString enumerateAttribute:RCTLiveMarkdownMentionUserAttributeName - inRange:NSMakeRange(0, attributedString.length) - options:0 - usingBlock:^(id value, NSRange range, BOOL *stop) { - if (value) { - [mentionRanges addObject:[NSValue valueWithRange:range]]; - } - }]; - - [_markdownUtils.markdownStyle.mentionUserBackgroundColor setFill]; - UIBezierPath *fullPath = [UIBezierPath new]; - - [self.textLineFragments enumerateObjectsUsingBlock:^(NSTextLineFragment * _Nonnull lineFragment, NSUInteger idx, BOOL * _Nonnull stop) { - if (lineFragment.characterRange.length == 0) { - return; - } - - CGRect lineBounds = lineFragment.typographicBounds; - for (NSValue *rangeValue in mentionRanges) { - NSRange mentionRange = [rangeValue rangeValue]; - NSRange intersection = NSIntersectionRange(lineFragment.characterRange, mentionRange); - 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(kCornerRadius, kCornerRadius)]; - - [fullPath appendPath:linePath]; - } - }]; - - [fullPath fill]; - [super drawAtPoint:point inContext:ctx]; -} - -@end diff --git a/apple/RCTMarkdownStyle.h b/apple/RCTMarkdownStyle.h index fd88fcaf..f57fa41a 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 f56f01ae..4598d945 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/src/styleUtils.ts b/src/styleUtils.ts index 7396dce7..489e7673 100644 --- a/src/styleUtils.ts +++ b/src/styleUtils.ts @@ -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, From 9d0400824cf3588caacc04b84739c76298fbbfc6 Mon Sep 17 00:00:00 2001 From: war-in Date: Thu, 14 Aug 2025 14:28:52 +0200 Subject: [PATCH 05/24] fix web styles test --- WebExample/__tests__/styles.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WebExample/__tests__/styles.spec.ts b/WebExample/__tests__/styles.spec.ts index d54334a9..8db7a6a4 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}) => { From 5a7c73c89011db89b64bb0fd88b938a6f8d74378 Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 19 Aug 2025 13:22:31 +0200 Subject: [PATCH 06/24] rename attribute & use custom objects instead of dict --- apple/MarkdownFormatter.h | 8 +++-- apple/MarkdownFormatter.mm | 30 +++++++++++++------ apple/MarkdownTextLayoutFragment.h | 6 ++++ apple/MarkdownTextLayoutFragment.mm | 34 ++++++++++------------ apple/MarkdownTextLayoutManagerDelegate.mm | 2 +- 5 files changed, 49 insertions(+), 31 deletions(-) diff --git a/apple/MarkdownFormatter.h b/apple/MarkdownFormatter.h index b27f2e13..c9098a12 100644 --- a/apple/MarkdownFormatter.h +++ b/apple/MarkdownFormatter.h @@ -6,9 +6,13 @@ NS_ASSUME_NONNULL_BEGIN const NSAttributedStringKey RCTLiveMarkdownTextAttributeName = @"RCTLiveMarkdownText"; -const NSAttributedStringKey RCTLiveMarkdownBlockquoteDepthAttributeName = @"RCTLiveMarkdownBlockquoteDepth"; +const NSAttributedStringKey RCTLiveMarkdownTextBackgroundAttributeName = @"RCTLiveMarkdownTextBackground"; +@interface RCTMarkdownTextBackground : NSObject + @property (nonatomic, strong) UIColor *color; + @property (nonatomic, assign) CGFloat cornerRadius; +@end -const NSAttributedStringKey RCTLiveMarkdownMentionAttributeName = @"RCTLiveMarkdownMention"; +const NSAttributedStringKey RCTLiveMarkdownBlockquoteDepthAttributeName = @"RCTLiveMarkdownBlockquoteDepth"; @interface MarkdownFormatter : NSObject diff --git a/apple/MarkdownFormatter.mm b/apple/MarkdownFormatter.mm index 86c8a95f..2eb4b72e 100644 --- a/apple/MarkdownFormatter.mm +++ b/apple/MarkdownFormatter.mm @@ -1,6 +1,9 @@ #import "MarkdownFormatter.h" #import +@implementation RCTMarkdownTextBackground +@end + @implementation MarkdownFormatter - (void)formatAttributedString:(nonnull NSMutableAttributedString *)attributedString @@ -92,9 +95,12 @@ - (void)applyRangeToAttributedString:(NSMutableAttributedString *)attributedStri } else if (type == "mention-here") { [attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.mentionHereColor range:range]; if (@available(iOS 16.0, *)) { - [attributedString addAttribute:RCTLiveMarkdownMentionAttributeName - value:@{@"backgroundColor": markdownStyle.mentionHereBackgroundColor, - @"cornerRadius": @(markdownStyle.mentionHereBorderRadius)} + RCTMarkdownTextBackground *textBackground = [[RCTMarkdownTextBackground alloc] init]; + textBackground.color = markdownStyle.mentionHereBackgroundColor; + textBackground.cornerRadius = markdownStyle.mentionHereBorderRadius; + + [attributedString addAttribute:RCTLiveMarkdownTextBackgroundAttributeName + value:textBackground range:range]; } else { [attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.mentionHereBackgroundColor range:range]; @@ -103,9 +109,12 @@ - (void)applyRangeToAttributedString:(NSMutableAttributedString *)attributedStri // TODO: change mention color when it mentions current user [attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.mentionUserColor range:range]; if (@available(iOS 16.0, *)) { - [attributedString addAttribute:RCTLiveMarkdownMentionAttributeName - value:@{@"backgroundColor": markdownStyle.mentionUserBackgroundColor, - @"cornerRadius": @(markdownStyle.mentionUserBorderRadius)} + RCTMarkdownTextBackground *textBackground = [[RCTMarkdownTextBackground alloc] init]; + textBackground.color = markdownStyle.mentionUserBackgroundColor; + textBackground.cornerRadius = markdownStyle.mentionUserBorderRadius; + + [attributedString addAttribute:RCTLiveMarkdownTextBackgroundAttributeName + value:textBackground range:range]; } else { [attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.mentionUserBackgroundColor range:range]; @@ -113,9 +122,12 @@ - (void)applyRangeToAttributedString:(NSMutableAttributedString *)attributedStri } else if (type == "mention-report") { [attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.mentionReportColor range:range]; if (@available(iOS 16.0, *)) { - [attributedString addAttribute:RCTLiveMarkdownMentionAttributeName - value:@{@"backgroundColor": markdownStyle.mentionReportBackgroundColor, - @"cornerRadius": @(markdownStyle.mentionReportBorderRadius)} + RCTMarkdownTextBackground *textBackground = [[RCTMarkdownTextBackground alloc] init]; + textBackground.color = markdownStyle.mentionReportBackgroundColor; + textBackground.cornerRadius = markdownStyle.mentionReportBorderRadius; + + [attributedString addAttribute:RCTLiveMarkdownTextBackgroundAttributeName + value:textBackground range:range]; } else { [attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.mentionReportBackgroundColor range:range]; diff --git a/apple/MarkdownTextLayoutFragment.h b/apple/MarkdownTextLayoutFragment.h index 29828564..95bc4f85 100644 --- a/apple/MarkdownTextLayoutFragment.h +++ b/apple/MarkdownTextLayoutFragment.h @@ -1,8 +1,14 @@ #import +#import #import NS_ASSUME_NONNULL_BEGIN +@interface RCTMarkdownTextBackgroundWithRange : NSObject +@property (nonatomic, assign) RCTMarkdownTextBackground *textBackground; +@property (nonatomic, assign) NSRange range; +@end + API_AVAILABLE(ios(15.0)) @interface MarkdownTextLayoutFragment : NSTextLayoutFragment diff --git a/apple/MarkdownTextLayoutFragment.mm b/apple/MarkdownTextLayoutFragment.mm index aec077bc..a3436221 100644 --- a/apple/MarkdownTextLayoutFragment.mm +++ b/apple/MarkdownTextLayoutFragment.mm @@ -1,5 +1,4 @@ #import -#import @implementation MarkdownTextLayoutFragment @@ -46,7 +45,7 @@ - (void)drawRibbon { } - (void)drawMentions { - NSMutableArray *mentions = [self getMentions]; + NSMutableArray *mentions = [self getMentions]; [self.textLineFragments enumerateObjectsUsingBlock:^(NSTextLineFragment * _Nonnull lineFragment, NSUInteger idx, BOOL * _Nonnull stop) { if (lineFragment.characterRange.length == 0) { @@ -54,18 +53,14 @@ - (void)drawMentions { } 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); + for (RCTMarkdownTextBackgroundWithRange *mention in mentions) { + NSRange intersection = NSIntersectionRange(lineFragment.characterRange, mention.range); if (intersection.length == 0) { continue; } - BOOL isStart = (intersection.location == mentionRange.location); - BOOL isEnd = (NSMaxRange(intersection) == NSMaxRange(mentionRange)); + BOOL isStart = (intersection.location == mention.range.location); + BOOL isEnd = (NSMaxRange(intersection) == NSMaxRange(mention.range)); CGPoint startLocation = [lineFragment locationForCharacterAtIndex:intersection.location]; CGPoint endLocation = [lineFragment locationForCharacterAtIndex:intersection.location + intersection.length]; @@ -87,9 +82,9 @@ - (void)drawMentions { UIBezierPath *linePath; linePath = [UIBezierPath bezierPathWithRoundedRect:paddedRect byRoundingCorners:cornersToRound - cornerRadii:CGSizeMake(cornerRadius, cornerRadius)]; + cornerRadii:CGSizeMake(mention.textBackground.cornerRadius, mention.textBackground.cornerRadius)]; - [backgroundColor setFill]; + [mention.textBackground.color setFill]; [linePath fill]; } }]; @@ -122,20 +117,21 @@ - (CGRect)boundingRect { return fragmentTextBounds; } -- (NSMutableArray*)getMentions { +- (NSMutableArray*)getMentions { NSTextParagraph *paragraph = (NSTextParagraph *)self.textElement; NSAttributedString *attributedString = [paragraph attributedString]; - NSMutableArray *mentions = [NSMutableArray array]; - [attributedString enumerateAttribute:RCTLiveMarkdownMentionAttributeName + NSMutableArray *mentions = [NSMutableArray array]; + [attributedString enumerateAttribute:RCTLiveMarkdownTextBackgroundAttributeName inRange:NSMakeRange(0, attributedString.length) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) { if (value) { - [mentions addObject:@{ - @"range": [NSValue valueWithRange:range], - @"value": value - }]; + RCTMarkdownTextBackgroundWithRange *textBackgroundWithRange = [[RCTMarkdownTextBackgroundWithRange alloc] init]; + textBackgroundWithRange.textBackground = value; + textBackgroundWithRange.range = range; + + [mentions addObject:textBackgroundWithRange]; } }]; diff --git a/apple/MarkdownTextLayoutManagerDelegate.mm b/apple/MarkdownTextLayoutManagerDelegate.mm index 67e0e31b..ed21b4f6 100644 --- a/apple/MarkdownTextLayoutManagerDelegate.mm +++ b/apple/MarkdownTextLayoutManagerDelegate.mm @@ -26,7 +26,7 @@ - (BOOL)hasMention:(NSTextElement *)textElement { NSAttributedString *attributedString = [paragraph attributedString]; __block BOOL hasMention = NO; - [attributedString enumerateAttribute:RCTLiveMarkdownMentionAttributeName + [attributedString enumerateAttribute:RCTLiveMarkdownTextBackgroundAttributeName inRange:NSMakeRange(0, attributedString.length) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) { From ddeba5155452366d035f75cec8590b1ad71b9e59 Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 19 Aug 2025 16:31:39 +0200 Subject: [PATCH 07/24] address review --- apple/MarkdownTextLayoutFragment.h | 9 ++- apple/MarkdownTextLayoutFragment.mm | 82 +++++++++------------- apple/MarkdownTextLayoutManagerDelegate.mm | 39 +++++----- src/styleUtils.ts | 6 +- 4 files changed, 61 insertions(+), 75 deletions(-) diff --git a/apple/MarkdownTextLayoutFragment.h b/apple/MarkdownTextLayoutFragment.h index 95bc4f85..c4abbe98 100644 --- a/apple/MarkdownTextLayoutFragment.h +++ b/apple/MarkdownTextLayoutFragment.h @@ -5,15 +5,18 @@ NS_ASSUME_NONNULL_BEGIN @interface RCTMarkdownTextBackgroundWithRange : NSObject -@property (nonatomic, assign) RCTMarkdownTextBackground *textBackground; -@property (nonatomic, assign) NSRange range; +@property (nonnull, atomic) RCTMarkdownTextBackground *textBackground; +@property NSRange range; @end API_AVAILABLE(ios(15.0)) @interface MarkdownTextLayoutFragment : NSTextLayoutFragment @property (nonnull, atomic) RCTMarkdownUtils *markdownUtils; -@property (nonnull, atomic) NSNumber* depth; + +@property NSUInteger depth; + +@property NSMutableArray *mentions; @end diff --git a/apple/MarkdownTextLayoutFragment.mm b/apple/MarkdownTextLayoutFragment.mm index a3436221..51af3369 100644 --- a/apple/MarkdownTextLayoutFragment.mm +++ b/apple/MarkdownTextLayoutFragment.mm @@ -1,11 +1,14 @@ #import +@implementation RCTMarkdownTextBackgroundWithRange +@end + @implementation MarkdownTextLayoutFragment #pragma mark - overriding class methods - (CGRect)renderingSurfaceBounds { - if (self.depth == nil) { + if (self.depth == 0) { return [super renderingSurfaceBounds]; } return CGRectUnion(self.boundingRect, [super renderingSurfaceBounds]); @@ -17,7 +20,7 @@ - (void)drawAtPoint:(CGPoint)point inContext:(CGContextRef)ctx { return; } - [self drawRibbon]; + [self drawBlockquoteRibbons]; [self drawMentions]; [super drawAtPoint:point inContext:ctx]; @@ -25,8 +28,8 @@ - (void)drawAtPoint:(CGPoint)point inContext:(CGContextRef)ctx { #pragma mark - drawing custom elements -- (void)drawRibbon { - if (self.depth == nil) { +- (void)drawBlockquoteRibbons { + if (self.depth == 0) { return; } @@ -38,14 +41,17 @@ - (void)drawRibbon { [_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); + 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 { - NSMutableArray *mentions = [self getMentions]; + if (self.mentions.count == 0) { + return; + } [self.textLineFragments enumerateObjectsUsingBlock:^(NSTextLineFragment * _Nonnull lineFragment, NSUInteger idx, BOOL * _Nonnull stop) { if (lineFragment.characterRange.length == 0) { @@ -53,36 +59,37 @@ - (void)drawMentions { } CGRect lineBounds = lineFragment.typographicBounds; - for (RCTMarkdownTextBackgroundWithRange *mention in mentions) { + for (RCTMarkdownTextBackgroundWithRange *mention in self.mentions) { NSRange intersection = NSIntersectionRange(lineFragment.characterRange, mention.range); if (intersection.length == 0) { continue; } - BOOL isStart = (intersection.location == mention.range.location); - BOOL isEnd = (NSMaxRange(intersection) == NSMaxRange(mention.range)); - CGPoint startLocation = [lineFragment locationForCharacterAtIndex:intersection.location]; CGPoint endLocation = [lineFragment locationForCharacterAtIndex:intersection.location + intersection.length]; + CGFloat width = endLocation.x - startLocation.x; - CGRect paddedRect = CGRectMake(startLocation.x, - lineBounds.origin.y, - endLocation.x - startLocation.x, - lineBounds.size.height); + CGRect backgroundRect = CGRectMake(startLocation.x, + lineBounds.origin.y, + width, + lineBounds.size.height); + BOOL isStart = (intersection.location == mention.range.location); + BOOL isEnd = (NSMaxRange(intersection) == NSMaxRange(mention.range)); UIRectCorner cornersToRound = 0; - if (isStart && isEnd) { - cornersToRound = UIRectCornerAllCorners; - } else if (isStart) { - cornersToRound = (UIRectCornerTopLeft | UIRectCornerBottomLeft); - } else if (isEnd) { - cornersToRound = (UIRectCornerTopRight | UIRectCornerBottomRight); + if (isStart) { + cornersToRound |= UIRectCornerTopLeft | UIRectCornerBottomLeft; + } + if (isEnd) { + cornersToRound |= UIRectCornerTopRight | UIRectCornerBottomRight; } - UIBezierPath *linePath; - linePath = [UIBezierPath bezierPathWithRoundedRect:paddedRect - byRoundingCorners:cornersToRound - cornerRadii:CGSizeMake(mention.textBackground.cornerRadius, mention.textBackground.cornerRadius)]; + UIBezierPath *linePath = (cornersToRound == 0) + ? [UIBezierPath bezierPathWithRect:backgroundRect] + : [UIBezierPath bezierPathWithRoundedRect:backgroundRect + byRoundingCorners:cornersToRound + cornerRadii:CGSizeMake(mention.textBackground.cornerRadius, + mention.textBackground.cornerRadius)]; [mention.textBackground.color setFill]; [linePath fill]; @@ -111,31 +118,10 @@ - (CGRect)boundingRect { 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); + fragmentTextBounds.origin.x -= (paddingLeft + borderWidth) + shift * (_depth - 1); + fragmentTextBounds.size.width = borderWidth + shift * (_depth - 1); return fragmentTextBounds; } -- (NSMutableArray*)getMentions { - NSTextParagraph *paragraph = (NSTextParagraph *)self.textElement; - NSAttributedString *attributedString = [paragraph 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]; - } - }]; - - return mentions; -} - @end diff --git a/apple/MarkdownTextLayoutManagerDelegate.mm b/apple/MarkdownTextLayoutManagerDelegate.mm index ed21b4f6..5c77b28a 100644 --- a/apple/MarkdownTextLayoutManagerDelegate.mm +++ b/apple/MarkdownTextLayoutManagerDelegate.mm @@ -9,34 +9,31 @@ - (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]; - BOOL hasMention = [self hasMention:textElement]; - if (depth != nil || hasMention) { + 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; + textLayoutFragment.depth = depth != nil ? [depth unsignedIntValue] : 0; + textLayoutFragment.mentions = mentions; 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:RCTLiveMarkdownTextBackgroundAttributeName - inRange:NSMakeRange(0, attributedString.length) - options:0 - usingBlock:^(id value, NSRange range, BOOL *stop) { - if (value) { - hasMention = YES; - *stop = YES; - } - }]; - - return hasMention; -} - @end diff --git a/src/styleUtils.ts b/src/styleUtils.ts index 489e7673..c97fd1e4 100644 --- a/src/styleUtils.ts +++ b/src/styleUtils.ts @@ -62,17 +62,17 @@ function makeDefaultMarkdownStyle(): MarkdownStyle { mentionHere: { color: 'green', backgroundColor: 'lime', - borderRadius: 5.0, + borderRadius: 5, }, mentionUser: { color: 'blue', backgroundColor: 'cyan', - borderRadius: 5.0, + borderRadius: 5, }, mentionReport: { color: 'red', backgroundColor: 'pink', - borderRadius: 5.0, + borderRadius: 5, }, inlineImage: { minWidth: 50, From ac410eeda892e233355c153910ee06f58f6ca103 Mon Sep 17 00:00:00 2001 From: war-in Date: Wed, 20 Aug 2025 13:01:08 +0200 Subject: [PATCH 08/24] support rounded background on android --- .../livemarkdown/MarkdownFormatter.java | 6 +- .../expensify/livemarkdown/MarkdownStyle.java | 21 +++++ .../spans/MarkdownBackgroundSpan.java | 87 +++++++++++++++++++ 3 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 android/src/main/java/com/expensify/livemarkdown/spans/MarkdownBackgroundSpan.java diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java index 78b554cd..ffd3220d 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 ca71e51c..2d57ca32 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownStyle.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownStyle.java @@ -63,18 +63,24 @@ 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) { mSyntaxColor = parseColor(map, "syntax", "color", context); mLinkColor = parseColor(map, "link", "color", context); @@ -95,10 +101,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"); mMentionUserColor = parseColor(map, "mentionUser", "color", context); mMentionUserBackgroundColor = parseColor(map, "mentionUser", "backgroundColor", context); + mMentionUserBorderRadius = parseFloat(map, "mentionUser", "borderRadius"); mMentionReportColor = parseColor(map, "mentionReport", "color", context); mMentionReportBackgroundColor = parseColor(map, "mentionReport", "backgroundColor", context); + mMentionReportBorderRadius = parseFloat(map, "mentionReport", "borderRadius"); } private static int parseColor(@NonNull ReadableMap map, @NonNull String key, @NonNull String prop, @NonNull Context context) { @@ -213,6 +222,10 @@ public int getMentionHereBackgroundColor() { return mMentionHereBackgroundColor; } + public float getMentionHereBorderRadius() { + return mMentionHereBorderRadius; + } + @ColorInt public int getMentionUserColor() { return mMentionUserColor; @@ -223,6 +236,10 @@ public int getMentionUserBackgroundColor() { return mMentionUserBackgroundColor; } + public float getMentionUserBorderRadius() { + return mMentionUserBorderRadius; + } + @ColorInt public int getMentionReportColor() { return mMentionReportColor; @@ -232,4 +249,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 00000000..565aa0ce --- /dev/null +++ b/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownBackgroundSpan.java @@ -0,0 +1,87 @@ +package com.expensify.livemarkdown.spans; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; +import android.text.Spanned; +import android.text.style.LeadingMarginSpan; +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 cornerRadius; + + public MarkdownBackgroundSpan(@ColorInt int backgroundColor, float borderRadius, int mentionStart, int mentionEnd) { + this.backgroundColor = backgroundColor; + this.cornerRadius = borderRadius; + this.mentionStart = mentionStart; + this.mentionEnd = mentionEnd; + } + + @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 leadingMargin = getLeadingMargin(text, start, end); + + boolean mentionStarts = start <= mentionStart; + boolean mentionEnds = end >= mentionEnd; + + float startX = leadingMargin + (mentionStarts ? paint.measureText(text, start, mentionStart) : 0); + float endX = leadingMargin + paint.measureText(text, start, mentionEnds ? mentionEnd : end); + + int originalColor = paint.getColor(); + paint.setColor(backgroundColor); + + RectF lineRect = new RectF(startX, top, endX, bottom); + Path backgroundPath = new Path(); + backgroundPath.addRoundRect(lineRect, createRadii(mentionStarts, mentionEnds), Path.Direction.CW); + canvas.drawPath(backgroundPath, paint); + + paint.setColor(originalColor); + } + + private int getLeadingMargin(@NonNull CharSequence text, int start, int end) { + int leadingMargin = 0; + if (text instanceof Spanned spanned) { + LeadingMarginSpan[] marginSpans = spanned.getSpans(start, end, LeadingMarginSpan.class); + for (LeadingMarginSpan marginSpan : marginSpans) { + leadingMargin += marginSpan.getLeadingMargin(true); + } + } + return leadingMargin; + } + + private float[] createRadii(boolean roundedLeft, boolean roundedRight) { + float[] radii = new float[8]; + + if (roundedLeft) { + radii[0] = radii[1] = cornerRadius; // top-left + radii[6] = radii[7] = cornerRadius; // bottom-left + } + + if (roundedRight) { + radii[2] = radii[3] = cornerRadius; // top-right + radii[4] = radii[5] = cornerRadius; // bottom-right + } + + return radii; + } +} From 870b5da71c05328772348bc504097d4b78d603b1 Mon Sep 17 00:00:00 2001 From: war-in Date: Wed, 20 Aug 2025 13:23:58 +0200 Subject: [PATCH 09/24] move `RCTMarkdownTextBackground` & `RCTMarkdownTextBackgroundWithRange` to a separate file --- apple/MarkdownFormatter.h | 4 ---- apple/MarkdownFormatter.mm | 4 +--- apple/MarkdownTextLayoutFragment.h | 7 +------ apple/MarkdownTextLayoutFragment.mm | 4 +--- apple/MarkdownTextLayoutManagerDelegate.mm | 1 + apple/RCTMarkdownTextBackgroundUtils.h | 18 ++++++++++++++++++ apple/RCTMarkdownTextBackgroundUtils.mm | 7 +++++++ 7 files changed, 29 insertions(+), 16 deletions(-) create mode 100644 apple/RCTMarkdownTextBackgroundUtils.h create mode 100644 apple/RCTMarkdownTextBackgroundUtils.mm diff --git a/apple/MarkdownFormatter.h b/apple/MarkdownFormatter.h index c9098a12..aad961ee 100644 --- a/apple/MarkdownFormatter.h +++ b/apple/MarkdownFormatter.h @@ -7,10 +7,6 @@ NS_ASSUME_NONNULL_BEGIN const NSAttributedStringKey RCTLiveMarkdownTextAttributeName = @"RCTLiveMarkdownText"; const NSAttributedStringKey RCTLiveMarkdownTextBackgroundAttributeName = @"RCTLiveMarkdownTextBackground"; -@interface RCTMarkdownTextBackground : NSObject - @property (nonatomic, strong) UIColor *color; - @property (nonatomic, assign) CGFloat cornerRadius; -@end const NSAttributedStringKey RCTLiveMarkdownBlockquoteDepthAttributeName = @"RCTLiveMarkdownBlockquoteDepth"; diff --git a/apple/MarkdownFormatter.mm b/apple/MarkdownFormatter.mm index 2eb4b72e..f89c0b92 100644 --- a/apple/MarkdownFormatter.mm +++ b/apple/MarkdownFormatter.mm @@ -1,8 +1,6 @@ #import "MarkdownFormatter.h" #import - -@implementation RCTMarkdownTextBackground -@end +#import @implementation MarkdownFormatter diff --git a/apple/MarkdownTextLayoutFragment.h b/apple/MarkdownTextLayoutFragment.h index c4abbe98..7edcf4f0 100644 --- a/apple/MarkdownTextLayoutFragment.h +++ b/apple/MarkdownTextLayoutFragment.h @@ -1,14 +1,9 @@ #import -#import +#import #import NS_ASSUME_NONNULL_BEGIN -@interface RCTMarkdownTextBackgroundWithRange : NSObject -@property (nonnull, atomic) RCTMarkdownTextBackground *textBackground; -@property NSRange range; -@end - API_AVAILABLE(ios(15.0)) @interface MarkdownTextLayoutFragment : NSTextLayoutFragment diff --git a/apple/MarkdownTextLayoutFragment.mm b/apple/MarkdownTextLayoutFragment.mm index 51af3369..c0283aec 100644 --- a/apple/MarkdownTextLayoutFragment.mm +++ b/apple/MarkdownTextLayoutFragment.mm @@ -1,7 +1,5 @@ #import - -@implementation RCTMarkdownTextBackgroundWithRange -@end +#import @implementation MarkdownTextLayoutFragment diff --git a/apple/MarkdownTextLayoutManagerDelegate.mm b/apple/MarkdownTextLayoutManagerDelegate.mm index 5c77b28a..924318c2 100644 --- a/apple/MarkdownTextLayoutManagerDelegate.mm +++ b/apple/MarkdownTextLayoutManagerDelegate.mm @@ -1,6 +1,7 @@ #import #import #import +#import @implementation MarkdownTextLayoutManagerDelegate diff --git a/apple/RCTMarkdownTextBackgroundUtils.h b/apple/RCTMarkdownTextBackgroundUtils.h new file mode 100644 index 00000000..752f2b57 --- /dev/null +++ b/apple/RCTMarkdownTextBackgroundUtils.h @@ -0,0 +1,18 @@ +#import + +@interface RCTMarkdownTextBackground : NSObject + +@property (nonatomic, strong) UIColor *color; + +@property (nonatomic, assign) CGFloat cornerRadius; + +@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 00000000..0dfa78db --- /dev/null +++ b/apple/RCTMarkdownTextBackgroundUtils.mm @@ -0,0 +1,7 @@ +#import + +@implementation RCTMarkdownTextBackground +@end + +@implementation RCTMarkdownTextBackgroundWithRange +@end From 99cf1fbfab95c6ce36684d109d39ac06eb41af3d Mon Sep 17 00:00:00 2001 From: war-in Date: Wed, 20 Aug 2025 16:45:34 +0200 Subject: [PATCH 10/24] rename cornerRadius to borderRadius --- .../livemarkdown/spans/MarkdownBackgroundSpan.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownBackgroundSpan.java b/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownBackgroundSpan.java index 565aa0ce..253d504e 100644 --- a/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownBackgroundSpan.java +++ b/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownBackgroundSpan.java @@ -16,11 +16,11 @@ public class MarkdownBackgroundSpan implements MarkdownSpan, LineBackgroundSpan private final int backgroundColor; private final int mentionStart; private final int mentionEnd; - private final float cornerRadius; + private final float borderRadius; public MarkdownBackgroundSpan(@ColorInt int backgroundColor, float borderRadius, int mentionStart, int mentionEnd) { this.backgroundColor = backgroundColor; - this.cornerRadius = borderRadius; + this.borderRadius = borderRadius; this.mentionStart = mentionStart; this.mentionEnd = mentionEnd; } @@ -73,13 +73,13 @@ private float[] createRadii(boolean roundedLeft, boolean roundedRight) { float[] radii = new float[8]; if (roundedLeft) { - radii[0] = radii[1] = cornerRadius; // top-left - radii[6] = radii[7] = cornerRadius; // bottom-left + radii[0] = radii[1] = borderRadius; // top-left + radii[6] = radii[7] = borderRadius; // bottom-left } if (roundedRight) { - radii[2] = radii[3] = cornerRadius; // top-right - radii[4] = radii[5] = cornerRadius; // bottom-right + radii[2] = radii[3] = borderRadius; // top-right + radii[4] = radii[5] = borderRadius; // bottom-right } return radii; From 8b416a5ea5625e7fc9dd21422fac8e3898c54f93 Mon Sep 17 00:00:00 2001 From: war-in Date: Wed, 20 Aug 2025 16:48:10 +0200 Subject: [PATCH 11/24] rename cornerRadius to borderRadius iOS --- apple/MarkdownFormatter.mm | 6 +++--- apple/MarkdownTextLayoutFragment.mm | 4 ++-- apple/RCTMarkdownTextBackgroundUtils.h | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apple/MarkdownFormatter.mm b/apple/MarkdownFormatter.mm index f89c0b92..cf94500c 100644 --- a/apple/MarkdownFormatter.mm +++ b/apple/MarkdownFormatter.mm @@ -95,7 +95,7 @@ - (void)applyRangeToAttributedString:(NSMutableAttributedString *)attributedStri if (@available(iOS 16.0, *)) { RCTMarkdownTextBackground *textBackground = [[RCTMarkdownTextBackground alloc] init]; textBackground.color = markdownStyle.mentionHereBackgroundColor; - textBackground.cornerRadius = markdownStyle.mentionHereBorderRadius; + textBackground.borderRadius = markdownStyle.mentionHereBorderRadius; [attributedString addAttribute:RCTLiveMarkdownTextBackgroundAttributeName value:textBackground @@ -109,7 +109,7 @@ - (void)applyRangeToAttributedString:(NSMutableAttributedString *)attributedStri if (@available(iOS 16.0, *)) { RCTMarkdownTextBackground *textBackground = [[RCTMarkdownTextBackground alloc] init]; textBackground.color = markdownStyle.mentionUserBackgroundColor; - textBackground.cornerRadius = markdownStyle.mentionUserBorderRadius; + textBackground.borderRadius = markdownStyle.mentionUserBorderRadius; [attributedString addAttribute:RCTLiveMarkdownTextBackgroundAttributeName value:textBackground @@ -122,7 +122,7 @@ - (void)applyRangeToAttributedString:(NSMutableAttributedString *)attributedStri if (@available(iOS 16.0, *)) { RCTMarkdownTextBackground *textBackground = [[RCTMarkdownTextBackground alloc] init]; textBackground.color = markdownStyle.mentionReportBackgroundColor; - textBackground.cornerRadius = markdownStyle.mentionReportBorderRadius; + textBackground.borderRadius = markdownStyle.mentionReportBorderRadius; [attributedString addAttribute:RCTLiveMarkdownTextBackgroundAttributeName value:textBackground diff --git a/apple/MarkdownTextLayoutFragment.mm b/apple/MarkdownTextLayoutFragment.mm index c0283aec..f6b4ce15 100644 --- a/apple/MarkdownTextLayoutFragment.mm +++ b/apple/MarkdownTextLayoutFragment.mm @@ -86,8 +86,8 @@ - (void)drawMentions { ? [UIBezierPath bezierPathWithRect:backgroundRect] : [UIBezierPath bezierPathWithRoundedRect:backgroundRect byRoundingCorners:cornersToRound - cornerRadii:CGSizeMake(mention.textBackground.cornerRadius, - mention.textBackground.cornerRadius)]; + cornerRadii:CGSizeMake(mention.textBackground.borderRadius, + mention.textBackground.borderRadius)]; [mention.textBackground.color setFill]; [linePath fill]; diff --git a/apple/RCTMarkdownTextBackgroundUtils.h b/apple/RCTMarkdownTextBackgroundUtils.h index 752f2b57..369d6cfd 100644 --- a/apple/RCTMarkdownTextBackgroundUtils.h +++ b/apple/RCTMarkdownTextBackgroundUtils.h @@ -4,7 +4,7 @@ @property (nonatomic, strong) UIColor *color; -@property (nonatomic, assign) CGFloat cornerRadius; +@property (nonatomic, assign) CGFloat borderRadius; @end From 28b6a13e75c87782d3fc54824d47329db9599fae Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 22 Aug 2025 11:29:36 +0200 Subject: [PATCH 12/24] fix blockquote `\n` issue --- apple/MarkdownTextLayoutFragment.mm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apple/MarkdownTextLayoutFragment.mm b/apple/MarkdownTextLayoutFragment.mm index f6b4ce15..6212e942 100644 --- a/apple/MarkdownTextLayoutFragment.mm +++ b/apple/MarkdownTextLayoutFragment.mm @@ -66,8 +66,9 @@ - (void)drawMentions { CGPoint startLocation = [lineFragment locationForCharacterAtIndex:intersection.location]; CGPoint endLocation = [lineFragment locationForCharacterAtIndex:intersection.location + intersection.length]; CGFloat width = endLocation.x - startLocation.x; + CGFloat x = lineBounds.origin.x + startLocation.x; - CGRect backgroundRect = CGRectMake(startLocation.x, + CGRect backgroundRect = CGRectMake(x, lineBounds.origin.y, width, lineBounds.size.height); From 98dafdaaf9f2d9bd92df4dcd3e0c88222d481469 Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 26 Aug 2025 10:33:01 +0200 Subject: [PATCH 13/24] use StaticLayout to correctly calculate text position --- .../spans/MarkdownBackgroundSpan.java | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownBackgroundSpan.java b/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownBackgroundSpan.java index 253d504e..59d1ca4a 100644 --- a/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownBackgroundSpan.java +++ b/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownBackgroundSpan.java @@ -4,8 +4,8 @@ import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; -import android.text.Spanned; -import android.text.style.LeadingMarginSpan; +import android.text.StaticLayout; +import android.text.TextPaint; import android.text.style.LineBackgroundSpan; import androidx.annotation.ColorInt; @@ -18,11 +18,15 @@ public class MarkdownBackgroundSpan implements MarkdownSpan, LineBackgroundSpan 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 @@ -39,36 +43,28 @@ public void drawBackground( int end, int lnum ) { - int leadingMargin = getLeadingMargin(text, start, end); + if (layout == null || layout.getText() != text || layout.getWidth() != right || layout.getLineEnd(0) != end) { + layout = StaticLayout.Builder.obtain(text, start, end, (TextPaint) paint, right).build(); + + boolean mentionStarts = start <= mentionStart; + boolean mentionEnds = end >= mentionEnd; - boolean mentionStarts = start <= mentionStart; - boolean mentionEnds = end >= mentionEnd; + float startX = layout.getPrimaryHorizontal(mentionStarts ? mentionStart : start); + float endX = layout.getPrimaryHorizontal(mentionEnds ? mentionEnd : end); - float startX = leadingMargin + (mentionStarts ? paint.measureText(text, start, mentionStart) : 0); - float endX = leadingMargin + paint.measureText(text, start, mentionEnds ? mentionEnd : end); + RectF lineRect = new RectF(startX, top, endX, bottom); + backgroundPath.reset(); + backgroundPath.addRoundRect(lineRect, createRadii(mentionStarts, mentionEnds), Path.Direction.CW); + } int originalColor = paint.getColor(); paint.setColor(backgroundColor); - RectF lineRect = new RectF(startX, top, endX, bottom); - Path backgroundPath = new Path(); - backgroundPath.addRoundRect(lineRect, createRadii(mentionStarts, mentionEnds), Path.Direction.CW); canvas.drawPath(backgroundPath, paint); paint.setColor(originalColor); } - private int getLeadingMargin(@NonNull CharSequence text, int start, int end) { - int leadingMargin = 0; - if (text instanceof Spanned spanned) { - LeadingMarginSpan[] marginSpans = spanned.getSpans(start, end, LeadingMarginSpan.class); - for (LeadingMarginSpan marginSpan : marginSpans) { - leadingMargin += marginSpan.getLeadingMargin(true); - } - } - return leadingMargin; - } - private float[] createRadii(boolean roundedLeft, boolean roundedRight) { float[] radii = new float[8]; From a105c10b8265ac17f8947c5584f8985005ab88c2 Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 26 Aug 2025 12:59:12 +0200 Subject: [PATCH 14/24] create layout for the specific part of the text to improve performance --- .../spans/MarkdownBackgroundSpan.java | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownBackgroundSpan.java b/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownBackgroundSpan.java index 59d1ca4a..c1c30762 100644 --- a/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownBackgroundSpan.java +++ b/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownBackgroundSpan.java @@ -43,14 +43,21 @@ public void drawBackground( int end, int lnum ) { - if (layout == null || layout.getText() != text || layout.getWidth() != right || layout.getLineEnd(0) != end) { - layout = StaticLayout.Builder.obtain(text, start, end, (TextPaint) paint, right).build(); + 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(); - boolean mentionStarts = start <= mentionStart; - boolean mentionEnds = end >= mentionEnd; + int relativeMentionStart = mentionStart - start; + int relativeMentionEnd = mentionEnd - start; - float startX = layout.getPrimaryHorizontal(mentionStarts ? mentionStart : start); - float endX = layout.getPrimaryHorizontal(mentionEnds ? mentionEnd : end); + boolean mentionStarts = lineStart <= relativeMentionStart; + boolean mentionEnds = lineEnd >= relativeMentionEnd; + + float startX = layout.getPrimaryHorizontal(mentionStarts ? relativeMentionStart: lineStart); + float endX = layout.getPrimaryHorizontal(mentionEnds ? relativeMentionEnd : lineEnd); RectF lineRect = new RectF(startX, top, endX, bottom); backgroundPath.reset(); From 2a5069eac27f10e1f23753358f3316aef12a8102 Mon Sep 17 00:00:00 2001 From: war-in Date: Wed, 22 Oct 2025 16:45:39 +0200 Subject: [PATCH 15/24] feat: add support for rounded corners in singleline input on iOS --- apple/MarkdownTextInputDecoratorComponentView.mm | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/apple/MarkdownTextInputDecoratorComponentView.mm b/apple/MarkdownTextInputDecoratorComponentView.mm index 912c262a..52d19e96 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]; From cbab2528e54f34a71770d29d8458075705609c92 Mon Sep 17 00:00:00 2001 From: war-in Date: Mon, 27 Oct 2025 17:11:45 +0100 Subject: [PATCH 16/24] fix: rounded background issues on singeline input when mentions were off screen --- apple/MarkdownTextLayoutFragment.mm | 44 +++++++++++++++++++---------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/apple/MarkdownTextLayoutFragment.mm b/apple/MarkdownTextLayoutFragment.mm index 6212e942..14bd62a0 100644 --- a/apple/MarkdownTextLayoutFragment.mm +++ b/apple/MarkdownTextLayoutFragment.mm @@ -17,10 +17,10 @@ - (void)drawAtPoint:(CGPoint)point inContext:(CGContextRef)ctx { [super drawAtPoint:point inContext:ctx]; return; } - + [self drawBlockquoteRibbons]; [self drawMentions]; - + [super drawAtPoint:point inContext:ctx]; } @@ -30,14 +30,14 @@ - (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; @@ -50,29 +50,43 @@ - (void)drawMentions { if (self.mentions.count == 0) { return; } - + + bool isSingleline = [self.textLineFragments count] == 1; [self.textLineFragments enumerateObjectsUsingBlock:^(NSTextLineFragment * _Nonnull lineFragment, NSUInteger idx, BOOL * _Nonnull stop) { if (lineFragment.characterRange.length == 0) { return; } - + CGRect lineBounds = lineFragment.typographicBounds; + CGPoint lineEndLocation = [lineFragment locationForCharacterAtIndex: lineFragment.characterRange.length]; for (RCTMarkdownTextBackgroundWithRange *mention in self.mentions) { NSRange intersection = NSIntersectionRange(lineFragment.characterRange, mention.range); if (intersection.length == 0) { continue; } - + 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; - + CGRect backgroundRect = CGRectMake(x, lineBounds.origin.y, width, lineBounds.size.height); - + BOOL isStart = (intersection.location == mention.range.location); BOOL isEnd = (NSMaxRange(intersection) == NSMaxRange(mention.range)); UIRectCorner cornersToRound = 0; @@ -82,14 +96,14 @@ - (void)drawMentions { 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]; } @@ -111,15 +125,15 @@ - (CGRect)boundingRect { 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; } From f1b8c743d55a90c77c0ad68142bf605978f6721c Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 28 Oct 2025 14:43:19 +0100 Subject: [PATCH 17/24] fix: Android - wrap mentions tightly, don't highlight entire line height --- .../livemarkdown/spans/MarkdownBackgroundSpan.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownBackgroundSpan.java b/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownBackgroundSpan.java index c1c30762..bccd714b 100644 --- a/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownBackgroundSpan.java +++ b/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownBackgroundSpan.java @@ -59,7 +59,11 @@ public void drawBackground( float startX = layout.getPrimaryHorizontal(mentionStarts ? relativeMentionStart: lineStart); float endX = layout.getPrimaryHorizontal(mentionEnds ? relativeMentionEnd : lineEnd); - RectF lineRect = new RectF(startX, top, endX, bottom); + 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); } From 46ad23fe7bb366dd53230c1f732dfd5d90571c5b Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 28 Oct 2025 15:13:53 +0100 Subject: [PATCH 18/24] fix: iOS - wrap mentions tightly, don't highlight entire line height --- apple/MarkdownTextLayoutFragment.mm | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/apple/MarkdownTextLayoutFragment.mm b/apple/MarkdownTextLayoutFragment.mm index 14bd62a0..ef5f738b 100644 --- a/apple/MarkdownTextLayoutFragment.mm +++ b/apple/MarkdownTextLayoutFragment.mm @@ -82,10 +82,20 @@ - (void)drawMentions { CGFloat width = endLocation.x - startLocation.x; CGFloat x = lineBounds.origin.x + startLocation.x; + NSUInteger lineRelativeLocation = intersection.location - lineFragment.characterRange.location; + UIFont *font = [lineFragment.attributedString attribute:NSFontAttributeName + atIndex:lineRelativeLocation + effectiveRange:NULL]; + CGFloat ascent = font.ascender; + CGFloat descent = font.descender; + CGFloat textHeight = ascent - descent; + CGFloat y = (startLocation.y - ascent) / 2; + + CGRect backgroundRect = CGRectMake(x, - lineBounds.origin.y, + y, width, - lineBounds.size.height); + textHeight); BOOL isStart = (intersection.location == mention.range.location); BOOL isEnd = (NSMaxRange(intersection) == NSMaxRange(mention.range)); From fe24512983f5ff86b70937fee32c9e36e250e94a Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 28 Oct 2025 15:46:23 +0100 Subject: [PATCH 19/24] fix: iOS - highlighting multiline mentions --- apple/MarkdownTextLayoutFragment.mm | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apple/MarkdownTextLayoutFragment.mm b/apple/MarkdownTextLayoutFragment.mm index ef5f738b..10d66377 100644 --- a/apple/MarkdownTextLayoutFragment.mm +++ b/apple/MarkdownTextLayoutFragment.mm @@ -82,14 +82,13 @@ - (void)drawMentions { CGFloat width = endLocation.x - startLocation.x; CGFloat x = lineBounds.origin.x + startLocation.x; - NSUInteger lineRelativeLocation = intersection.location - lineFragment.characterRange.location; UIFont *font = [lineFragment.attributedString attribute:NSFontAttributeName - atIndex:lineRelativeLocation + atIndex:intersection.location effectiveRange:NULL]; CGFloat ascent = font.ascender; CGFloat descent = font.descender; CGFloat textHeight = ascent - descent; - CGFloat y = (startLocation.y - ascent) / 2; + CGFloat y = lineBounds.size.height * idx + (startLocation.y - ascent) / 2; CGRect backgroundRect = CGRectMake(x, From 39e031876c225ca6230bbc9684dee8fb43f5a293 Mon Sep 17 00:00:00 2001 From: war-in Date: Wed, 29 Oct 2025 14:26:04 +0100 Subject: [PATCH 20/24] fix: Android - apply density to borderRadius to align radius on all devices --- .../java/com/expensify/livemarkdown/MarkdownStyle.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownStyle.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownStyle.java index 2d57ca32..2f2524df 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownStyle.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownStyle.java @@ -82,6 +82,8 @@ public class MarkdownStyle { 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"); @@ -101,13 +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"); + mMentionHereBorderRadius = parseFloat(map, "mentionHere", "borderRadius") * screenDensity; mMentionUserColor = parseColor(map, "mentionUser", "color", context); mMentionUserBackgroundColor = parseColor(map, "mentionUser", "backgroundColor", context); - mMentionUserBorderRadius = parseFloat(map, "mentionUser", "borderRadius"); + mMentionUserBorderRadius = parseFloat(map, "mentionUser", "borderRadius") * screenDensity; mMentionReportColor = parseColor(map, "mentionReport", "color", context); mMentionReportBackgroundColor = parseColor(map, "mentionReport", "backgroundColor", context); - mMentionReportBorderRadius = parseFloat(map, "mentionReport", "borderRadius"); + mMentionReportBorderRadius = parseFloat(map, "mentionReport", "borderRadius") * screenDensity; } private static int parseColor(@NonNull ReadableMap map, @NonNull String key, @NonNull String prop, @NonNull Context context) { From 0da82070c3533ba6b588bb469e3f6936956d22f7 Mon Sep 17 00:00:00 2001 From: war-in Date: Wed, 29 Oct 2025 16:12:33 +0100 Subject: [PATCH 21/24] chore: improve the iOS algorithm --- .../expensify/livemarkdown/MarkdownStyle.java | 2 +- apple/MarkdownTextLayoutFragment.mm | 26 +++++++++++++------ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownStyle.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownStyle.java index 2f2524df..34e9c632 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownStyle.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownStyle.java @@ -83,7 +83,7 @@ public class MarkdownStyle { 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"); diff --git a/apple/MarkdownTextLayoutFragment.mm b/apple/MarkdownTextLayoutFragment.mm index 10d66377..702e9fbc 100644 --- a/apple/MarkdownTextLayoutFragment.mm +++ b/apple/MarkdownTextLayoutFragment.mm @@ -52,19 +52,30 @@ - (void)drawMentions { } 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]; - for (RCTMarkdownTextBackgroundWithRange *mention in self.mentions) { - NSRange intersection = NSIntersectionRange(lineFragment.characterRange, mention.range); - if (intersection.length == 0) { - continue; + + 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 @@ -86,9 +97,8 @@ - (void)drawMentions { atIndex:intersection.location effectiveRange:NULL]; CGFloat ascent = font.ascender; - CGFloat descent = font.descender; - CGFloat textHeight = ascent - descent; - CGFloat y = lineBounds.size.height * idx + (startLocation.y - ascent) / 2; + CGFloat textHeight = font.lineHeight; + CGFloat y = lineBounds.origin.y + startLocation.y - ascent; CGRect backgroundRect = CGRectMake(x, From 6665f831480b6fef2efd121e3dd04cccb21b5db3 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 14 Nov 2025 12:21:42 +0100 Subject: [PATCH 22/24] chore: iOS - apply review --- apple/MarkdownFormatter.mm | 73 +++++++++---------- ...MarkdownTextInputDecoratorComponentView.mm | 8 +- apple/MarkdownTextLayoutFragment.h | 2 +- apple/MarkdownTextLayoutFragment.mm | 3 +- apple/MarkdownTextLayoutManagerDelegate.mm | 2 +- apple/RCTMarkdownTextBackground.h | 9 +++ apple/RCTMarkdownTextBackground.mm | 4 + apple/RCTMarkdownTextBackgroundUtils.mm | 7 -- ...h => RCTMarkdownTextBackgroundWithRange.h} | 10 +-- apple/RCTMarkdownTextBackgroundWithRange.mm | 5 ++ 10 files changed, 64 insertions(+), 59 deletions(-) create mode 100644 apple/RCTMarkdownTextBackground.h create mode 100644 apple/RCTMarkdownTextBackground.mm delete mode 100644 apple/RCTMarkdownTextBackgroundUtils.mm rename apple/{RCTMarkdownTextBackgroundUtils.h => RCTMarkdownTextBackgroundWithRange.h} (55%) create mode 100644 apple/RCTMarkdownTextBackgroundWithRange.mm diff --git a/apple/MarkdownFormatter.mm b/apple/MarkdownFormatter.mm index cf94500c..350653c5 100644 --- a/apple/MarkdownFormatter.mm +++ b/apple/MarkdownFormatter.mm @@ -1,6 +1,6 @@ #import "MarkdownFormatter.h" #import -#import +#import @implementation MarkdownFormatter @@ -91,45 +91,24 @@ - (void)applyRangeToAttributedString:(NSMutableAttributedString *)attributedStri [attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.codeColor range:range]; [attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.codeBackgroundColor range:range]; } else if (type == "mention-here") { - [attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.mentionHereColor 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]; - } + [self applyMentionFormatting:attributedString + range:range + mentionColor:markdownStyle.mentionHereColor + backgroundColor:markdownStyle.mentionHereBackgroundColor + borderRadius:markdownStyle.mentionHereBorderRadius]; } else if (type == "mention-user") { // TODO: change mention color when it mentions current user - [attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.mentionUserColor 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]; - } + [self applyMentionFormatting:attributedString + range:range + mentionColor:markdownStyle.mentionUserColor + backgroundColor:markdownStyle.mentionUserBackgroundColor + borderRadius:markdownStyle.mentionUserBorderRadius]; } else if (type == "mention-report") { - [attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.mentionReportColor 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]; - } + [self applyMentionFormatting:attributedString + range:range + mentionColor:markdownStyle.mentionReportColor + backgroundColor:markdownStyle.mentionReportBackgroundColor + borderRadius:markdownStyle.mentionReportBorderRadius]; } else if (type == "link") { [attributedString addAttribute:NSUnderlineStyleAttributeName value:[NSNumber numberWithInteger:NSUnderlineStyleSingle] range:range]; [attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.linkColor range:range]; @@ -149,6 +128,26 @@ - (void)applyRangeToAttributedString:(NSMutableAttributedString *)attributedStri } } +- (void)applyMentionFormatting:(NSMutableAttributedString *)attributedString + range:(const NSRange)range + mentionColor:(UIColor *)mentionColor + backgroundColor:(UIColor *)backgroundColor + borderRadius:(CGFloat)borderRadius +{ + [attributedString addAttribute:NSForegroundColorAttributeName value:mentionColor range:range]; + if (@available(iOS 16.0, *)) { + RCTMarkdownTextBackground *textBackground = [[RCTMarkdownTextBackground alloc] init]; + textBackground.color = backgroundColor; + textBackground.borderRadius = borderRadius; + + [attributedString addAttribute:RCTLiveMarkdownTextBackgroundAttributeName + value:textBackground + range:range]; + } else { + [attributedString addAttribute:NSBackgroundColorAttributeName value:backgroundColor range:range]; + } +} + static void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText, NSRange attributedTextRange) { __block CGFloat maximumLineHeight = 0; diff --git a/apple/MarkdownTextInputDecoratorComponentView.mm b/apple/MarkdownTextInputDecoratorComponentView.mm index 52d19e96..ba5f8281 100644 --- a/apple/MarkdownTextInputDecoratorComponentView.mm +++ b/apple/MarkdownTextInputDecoratorComponentView.mm @@ -101,9 +101,11 @@ - (void)addTextInputObservers [_markdownTextFieldObserver textFieldDidChange:_textField]; if (@available(iOS 16.0, *)) { - NSTextStorage *textStorage = [_textField valueForKey:@"_textStorage"]; - NSTextContainer *textContainer = [_textField valueForKey:@"_textContainer"]; - NSTextLayoutManager *textLayoutManager = [textContainer valueForKey:@"_textLayoutManager"]; + std::string text = "txet"; + std::reverse(text.begin(), text.end()); + NSTextStorage *textStorage = [_textField valueForKey:[NSString stringWithFormat:@"%sStorage", text.c_str()]]; + NSTextContainer *textContainer = [_textField valueForKey:[NSString stringWithFormat:@"%sContainer", text.c_str()]]; + NSTextLayoutManager *textLayoutManager = [textContainer valueForKey:[NSString stringWithFormat:@"%sLayoutManager", text.c_str()]]; _markdownTextLayoutManagerDelegate = [[MarkdownTextLayoutManagerDelegate alloc] init]; _markdownTextLayoutManagerDelegate.textStorage = textStorage; diff --git a/apple/MarkdownTextLayoutFragment.h b/apple/MarkdownTextLayoutFragment.h index 7edcf4f0..4e2d098a 100644 --- a/apple/MarkdownTextLayoutFragment.h +++ b/apple/MarkdownTextLayoutFragment.h @@ -1,5 +1,5 @@ #import -#import +#import #import NS_ASSUME_NONNULL_BEGIN diff --git a/apple/MarkdownTextLayoutFragment.mm b/apple/MarkdownTextLayoutFragment.mm index 702e9fbc..26670b12 100644 --- a/apple/MarkdownTextLayoutFragment.mm +++ b/apple/MarkdownTextLayoutFragment.mm @@ -1,5 +1,5 @@ #import -#import +#import @implementation MarkdownTextLayoutFragment @@ -79,6 +79,7 @@ - (void)drawMentions { CGPoint startLocation = [lineFragment locationForCharacterAtIndex:intersection.location]; if (isSingleline && startLocation.x == 0 && intersection.location > 0) { // singleline: mention starts off screen, no need to draw background + // it only happens when the text input isn't focused continue; } diff --git a/apple/MarkdownTextLayoutManagerDelegate.mm b/apple/MarkdownTextLayoutManagerDelegate.mm index 924318c2..28fdade0 100644 --- a/apple/MarkdownTextLayoutManagerDelegate.mm +++ b/apple/MarkdownTextLayoutManagerDelegate.mm @@ -1,7 +1,7 @@ #import #import #import -#import +#import @implementation MarkdownTextLayoutManagerDelegate diff --git a/apple/RCTMarkdownTextBackground.h b/apple/RCTMarkdownTextBackground.h new file mode 100644 index 00000000..a9802fb4 --- /dev/null +++ b/apple/RCTMarkdownTextBackground.h @@ -0,0 +1,9 @@ +#import + +@interface RCTMarkdownTextBackground : NSObject + +@property (nonatomic, strong) UIColor *color; + +@property (nonatomic, assign) CGFloat borderRadius; + +@end diff --git a/apple/RCTMarkdownTextBackground.mm b/apple/RCTMarkdownTextBackground.mm new file mode 100644 index 00000000..2a48b415 --- /dev/null +++ b/apple/RCTMarkdownTextBackground.mm @@ -0,0 +1,4 @@ +#import + +@implementation RCTMarkdownTextBackground +@end diff --git a/apple/RCTMarkdownTextBackgroundUtils.mm b/apple/RCTMarkdownTextBackgroundUtils.mm deleted file mode 100644 index 0dfa78db..00000000 --- a/apple/RCTMarkdownTextBackgroundUtils.mm +++ /dev/null @@ -1,7 +0,0 @@ -#import - -@implementation RCTMarkdownTextBackground -@end - -@implementation RCTMarkdownTextBackgroundWithRange -@end diff --git a/apple/RCTMarkdownTextBackgroundUtils.h b/apple/RCTMarkdownTextBackgroundWithRange.h similarity index 55% rename from apple/RCTMarkdownTextBackgroundUtils.h rename to apple/RCTMarkdownTextBackgroundWithRange.h index 369d6cfd..461a979b 100644 --- a/apple/RCTMarkdownTextBackgroundUtils.h +++ b/apple/RCTMarkdownTextBackgroundWithRange.h @@ -1,13 +1,5 @@ #import - -@interface RCTMarkdownTextBackground : NSObject - -@property (nonatomic, strong) UIColor *color; - -@property (nonatomic, assign) CGFloat borderRadius; - -@end - +#import @interface RCTMarkdownTextBackgroundWithRange : NSObject diff --git a/apple/RCTMarkdownTextBackgroundWithRange.mm b/apple/RCTMarkdownTextBackgroundWithRange.mm new file mode 100644 index 00000000..c3b23cbb --- /dev/null +++ b/apple/RCTMarkdownTextBackgroundWithRange.mm @@ -0,0 +1,5 @@ +#import +#import + +@implementation RCTMarkdownTextBackgroundWithRange +@end From c556257cc99c74ff510f3a622268bd16e87af0b3 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 14 Nov 2025 12:35:15 +0100 Subject: [PATCH 23/24] chore: Android - apply review --- .../spans/MarkdownBackgroundSpan.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownBackgroundSpan.java b/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownBackgroundSpan.java index bccd714b..557fc719 100644 --- a/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownBackgroundSpan.java +++ b/android/src/main/java/com/expensify/livemarkdown/spans/MarkdownBackgroundSpan.java @@ -43,21 +43,21 @@ public void drawBackground( 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) { + if (layout == null || layout.getText() != lineText || layout.getWidth() != right || layout.getLineEnd(0) != lineText.length()) { + int currentLineStart = 0; + int currentLineEnd = lineText.length(); // Create layout for the current line only - layout = StaticLayout.Builder.obtain(lineText, lineStart, lineEnd, (TextPaint) paint, right).build(); + layout = StaticLayout.Builder.obtain(lineText, currentLineStart, currentLineEnd, (TextPaint) paint, right).build(); int relativeMentionStart = mentionStart - start; int relativeMentionEnd = mentionEnd - start; - boolean mentionStarts = lineStart <= relativeMentionStart; - boolean mentionEnds = lineEnd >= relativeMentionEnd; + boolean mentionStartsInCurrentLine = currentLineStart <= relativeMentionStart; + boolean mentionEndsInCurrentLine = currentLineEnd >= relativeMentionEnd; - float startX = layout.getPrimaryHorizontal(mentionStarts ? relativeMentionStart: lineStart); - float endX = layout.getPrimaryHorizontal(mentionEnds ? relativeMentionEnd : lineEnd); + float startX = layout.getPrimaryHorizontal(mentionStartsInCurrentLine ? relativeMentionStart: currentLineStart); + float endX = layout.getPrimaryHorizontal(mentionEndsInCurrentLine ? relativeMentionEnd : currentLineEnd); Paint.FontMetrics fm = paint.getFontMetrics(); float startY = baseline + fm.ascent; @@ -65,7 +65,7 @@ public void drawBackground( RectF lineRect = new RectF(startX, startY, endX, endY); backgroundPath.reset(); - backgroundPath.addRoundRect(lineRect, createRadii(mentionStarts, mentionEnds), Path.Direction.CW); + backgroundPath.addRoundRect(lineRect, createRadii(mentionStartsInCurrentLine, mentionEndsInCurrentLine), Path.Direction.CW); } int originalColor = paint.getColor(); From eb521b53582165d532918ca64b859210dc92bf09 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 28 Nov 2025 10:50:28 +0100 Subject: [PATCH 24/24] refactor: improve private field access logic in MarkdownTextInputDecoratorComponentView --- apple/MarkdownTextInputDecoratorComponentView.mm | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/apple/MarkdownTextInputDecoratorComponentView.mm b/apple/MarkdownTextInputDecoratorComponentView.mm index ba5f8281..b6cabe88 100644 --- a/apple/MarkdownTextInputDecoratorComponentView.mm +++ b/apple/MarkdownTextInputDecoratorComponentView.mm @@ -101,14 +101,16 @@ - (void)addTextInputObservers [_markdownTextFieldObserver textFieldDidChange:_textField]; if (@available(iOS 16.0, *)) { - std::string text = "txet"; - std::reverse(text.begin(), text.end()); - NSTextStorage *textStorage = [_textField valueForKey:[NSString stringWithFormat:@"%sStorage", text.c_str()]]; - NSTextContainer *textContainer = [_textField valueForKey:[NSString stringWithFormat:@"%sContainer", text.c_str()]]; - NSTextLayoutManager *textLayoutManager = [textContainer valueForKey:[NSString stringWithFormat:@"%sLayoutManager", text.c_str()]]; - + auto key = [](std::string s) { + std::reverse(s.begin(), s.end()); + return @(s.c_str()); + }; + + NSTextContainer *textContainer = [_textField valueForKey:key("reniatnoCtxet_")]; + NSTextLayoutManager *textLayoutManager = [textContainer valueForKey:key("reganaMtuoyaLtxet_")]; + _markdownTextLayoutManagerDelegate = [[MarkdownTextLayoutManagerDelegate alloc] init]; - _markdownTextLayoutManagerDelegate.textStorage = textStorage; + _markdownTextLayoutManagerDelegate.textStorage = [_textField valueForKey:key("egarotStxet_")]; _markdownTextLayoutManagerDelegate.markdownUtils = _markdownUtils; textLayoutManager.delegate = _markdownTextLayoutManagerDelegate; }