From 76bed9d7b66006cd7ff0f3385aab1f0f44d002d5 Mon Sep 17 00:00:00 2001 From: Maksymilian Galas Date: Fri, 12 Apr 2024 18:26:46 +0200 Subject: [PATCH] Implement code and pre blocks support on iOS --- ios/MarkdownLayoutManager.mm | 179 +++++++++++++++--- ios/RCTMarkdownStyle.h | 8 + ios/RCTMarkdownStyle.mm | 16 ++ ios/RCTMarkdownUtils.h | 2 + ios/RCTMarkdownUtils.mm | 14 +- ...wnTextInputDecoratorViewNativeComponent.ts | 8 + src/styleUtils.ts | 8 + 7 files changed, 200 insertions(+), 35 deletions(-) diff --git a/ios/MarkdownLayoutManager.mm b/ios/MarkdownLayoutManager.mm index 3974ba98..8b031fa8 100644 --- a/ios/MarkdownLayoutManager.mm +++ b/ios/MarkdownLayoutManager.mm @@ -2,39 +2,158 @@ @implementation MarkdownLayoutManager +- (BOOL)isRange:(NSRange)smallerRange inRange:(NSRange)largerRange { + NSUInteger start = smallerRange.location; + NSUInteger end = start + smallerRange.length; + NSUInteger location = largerRange.location; + return location >= start && location < end; +} + +- (CGRect)rectByAddingPadding:(CGFloat)padding toRect:(CGRect)rect { + rect.origin.x -= padding; + rect.origin.y -= padding; + rect.size.width += padding * 2; + rect.size.height += padding * 2; + return rect; +} + - (void)drawBackgroundForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin { - [super drawBackgroundForGlyphRange:glyphsToShow atPoint:origin]; - - [self enumerateLineFragmentsForGlyphRange:glyphsToShow usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer * _Nonnull textContainer, NSRange glyphRange, BOOL * _Nonnull stop) { - __block BOOL isBlockquote = NO; - __block int currentDepth = 0; - RCTMarkdownUtils *markdownUtils = [self valueForKey:@"markdownUtils"]; - [markdownUtils.blockquoteRangesAndLevels enumerateObjectsUsingBlock:^(NSDictionary *item, NSUInteger idx, BOOL * _Nonnull stop) { - NSRange range = [[item valueForKey:@"range"] rangeValue]; - currentDepth = [[item valueForKey:@"depth"] unsignedIntegerValue]; - NSUInteger start = range.location; - NSUInteger end = start + range.length; - NSUInteger location = glyphRange.location; - if (location >= start && location < end) { - isBlockquote = YES; - *stop = YES; - } + [super drawBackgroundForGlyphRange:glyphsToShow atPoint:origin]; + + RCTMarkdownStyle *style = [_markdownUtils markdownStyle]; + [self drawBlockquotesForRanges:[_markdownUtils blockquoteRangesAndLevels] andGlyphRange:glyphsToShow atPoint:origin withColor:[style blockquoteBorderColor] width:[style blockquoteBorderWidth] margin:[style blockquoteMarginLeft] andPadding:[style blockquotePaddingLeft]]; + [self drawPreBackgroundForRanges:[_markdownUtils preRanges] atPoint:origin withColor:[style preBackgroundColor] borderColor:[style preBorderColor] borderWidth:[style preBorderWidth] borderRadius:[style preBorderRadius] andPadding:[style prePadding]]; + [self drawCodeBackgroundForRanges:[_markdownUtils codeRanges] atPoint:origin withColor:[style codeBackgroundColor] borderColor:[style codeBorderColor] borderWidth:[style codeBorderWidth] borderRadius:[style codeBorderRadius] andPadding:[style codePadding]]; +} + +- (void)drawBlockquotesForRanges:(NSArray*)ranges andGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin withColor:(UIColor*)color width:(CGFloat)width margin:(CGFloat)margin andPadding:(CGFloat)padding { + [self enumerateLineFragmentsForGlyphRange:glyphsToShow usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer * _Nonnull textContainer, NSRange glyphRange, BOOL * _Nonnull stop) { + __block BOOL isBlockquote = NO; + __block int currentDepth = 0; + + [ranges enumerateObjectsUsingBlock:^(NSDictionary *item, NSUInteger idx, BOOL * _Nonnull stop) { + NSRange range = [[item valueForKey:@"range"] rangeValue]; + currentDepth = [[item valueForKey:@"depth"] unsignedIntegerValue]; + if ([self isRange:range inRange:glyphRange]) { + isBlockquote = YES; + *stop = YES; + } + }]; + if (isBlockquote) { + CGFloat paddingLeft = origin.x; + CGFloat paddingTop = origin.y; + CGFloat y = paddingTop + rect.origin.y; + CGFloat height = rect.size.height; + CGFloat shift = margin + width + padding; + for (int level = 0; level < currentDepth; level++) { + CGFloat x = paddingLeft + (level * shift) + margin; + CGRect lineRect = CGRectMake(x, y, width, height); + [color setFill]; + UIRectFill(lineRect); + } + } + }]; +} + +- (void)drawPreBackgroundForRanges:(NSArray*)ranges atPoint:(CGPoint)origin withColor:(UIColor*)backgroundColor borderColor:(UIColor*)borderColor borderWidth:(CGFloat)borderWidth borderRadius:(CGFloat)borderRadius andPadding:(CGFloat)padding { + __block CGRect preRect = CGRectNull; + [ranges enumerateObjectsUsingBlock:^(NSValue *item, NSUInteger idx, BOOL * _Nonnull stop) { + NSRange range = [item rangeValue]; + // We don't want the trailing ``` to be a part of the block so we need to reduce range by 1. + // This also breaks one character blocks so we need to check if range is larger. + if (range.length > 1) { + range.location += 1; + range.length -= 1; + } + + [self enumerateLineFragmentsForGlyphRange:range usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer * _Nonnull textContainer, NSRange glyphRange, BOOL * _Nonnull stop) { + if (CGRectIsNull(preRect)) { + preRect = usedRect; + CGFloat paddingLeft = origin.x; + preRect.origin.x += paddingLeft; + CGFloat paddingTop = origin.y; + preRect.origin.y += paddingTop; + } else { + CGFloat usedWidth = usedRect.size.width; + if (usedWidth > preRect.size.width) { + preRect.size.width = usedWidth; + } + preRect.size.height += usedRect.size.height; + } + }]; + + if (!CGRectIsNull(preRect)) { + preRect = [self rectByAddingPadding:padding toRect:preRect]; + [self drawBackgroundWithColor:backgroundColor borderColor:borderColor borderWidth:borderWidth andBorderRadius:borderRadius forRect:preRect isLeftOpen:NO isRightOpen:NO]; + preRect = CGRectNull; + } + }]; +} + +- (void)drawCodeBackgroundForRanges:(NSArray*)ranges atPoint:(CGPoint)origin withColor:(UIColor*)backgroundColor borderColor:(UIColor*)borderColor borderWidth:(CGFloat)borderWidth borderRadius:(CGFloat)borderRadius andPadding:(CGFloat)padding { + [ranges enumerateObjectsUsingBlock:^(NSValue *item, NSUInteger idx, BOOL * _Nonnull stop) { + NSRange range = [item rangeValue]; + [self enumerateLineFragmentsForGlyphRange:range usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer * _Nonnull textContainer, NSRange glyphRange, BOOL * _Nonnull stop) { + BOOL isLeftSideOpen = YES; + BOOL isRightSideOpen = YES; + + NSRange adjustedRange = glyphRange; + if (range.location > adjustedRange.location) { + adjustedRange.length -= range.location - adjustedRange.location; + adjustedRange.location = range.location; + isLeftSideOpen = NO; + } + + NSUInteger rangeEndLocation = range.location + range.length; + NSUInteger adjustedRangeEndLocation = adjustedRange.location + adjustedRange.length; + if (rangeEndLocation < adjustedRangeEndLocation) { + adjustedRange.length -= adjustedRangeEndLocation - rangeEndLocation; + isRightSideOpen = NO; + } + + CGRect codeRect = [self boundingRectForGlyphRange:adjustedRange inTextContainer:textContainer]; + CGFloat paddingLeft = origin.x; + codeRect.origin.x += paddingLeft; + CGFloat paddingTop = origin.y; + codeRect.origin.y += paddingTop; + codeRect = [self rectByAddingPadding:padding toRect:codeRect]; + [self drawBackgroundWithColor:backgroundColor borderColor:borderColor borderWidth:borderWidth andBorderRadius:borderRadius forRect:codeRect isLeftOpen:isLeftSideOpen isRightOpen:isRightSideOpen]; + }]; }]; - if (isBlockquote) { - CGFloat paddingLeft = origin.x; - CGFloat paddingTop = origin.y; - CGFloat y = paddingTop + rect.origin.y; - CGFloat width = markdownUtils.markdownStyle.blockquoteBorderWidth; - CGFloat height = rect.size.height; - CGFloat shift = markdownUtils.markdownStyle.blockquoteMarginLeft + markdownUtils.markdownStyle.blockquoteBorderWidth + markdownUtils.markdownStyle.blockquotePaddingLeft; - for (int level = 0; level < currentDepth; level++) { - CGFloat x = paddingLeft + (level * shift) + markdownUtils.markdownStyle.blockquoteMarginLeft; - CGRect lineRect = CGRectMake(x, y, width, height); - [markdownUtils.markdownStyle.blockquoteBorderColor setFill]; - UIRectFill(lineRect); - } +} + +- (void)drawBackgroundWithColor:(UIColor*)backgroundColor borderColor:(UIColor*)borderColor borderWidth:(CGFloat)borderWidth andBorderRadius:(CGFloat)radius forRect:(CGRect)rect isLeftOpen:(BOOL)isLeftOpen isRightOpen:(BOOL)isRightOpen { + UIRectCorner corners = 0; + if (!isLeftOpen) { + corners |= UIRectCornerTopLeft | UIRectCornerBottomLeft; } - }]; + if (!isRightOpen) { + corners |= UIRectCornerTopRight | UIRectCornerBottomRight; + } + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:corners cornerRadii:CGSizeMake(radius, radius)]; + + [backgroundColor setFill]; + [path fill]; + [borderColor setStroke]; + [path setLineWidth:borderWidth]; + [path stroke]; + + if (isLeftOpen) { + [self openSideForRect:rect withBorderWidth:borderWidth isLeft:YES]; + } + if (isRightOpen) { + [self openSideForRect:rect withBorderWidth:borderWidth isLeft:NO]; + } +} + +- (void)openSideForRect:(CGRect)rect withBorderWidth:(CGFloat)borderWidth isLeft:(BOOL)isLeft { + UIBezierPath *path = [[UIBezierPath alloc] init]; + CGFloat x = isLeft ? CGRectGetMinX(rect) : CGRectGetMaxX(rect); + [path moveToPoint:CGPointMake(x, CGRectGetMinY(rect) - borderWidth)]; + [path addLineToPoint:CGPointMake(x, CGRectGetMaxY(rect) + borderWidth)]; + [[UIColor clearColor] setStroke]; + [path setLineWidth:borderWidth + 1]; + [path strokeWithBlendMode:kCGBlendModeClear alpha:1.0]; } @end diff --git a/ios/RCTMarkdownStyle.h b/ios/RCTMarkdownStyle.h index 0cb24933..d4356f81 100644 --- a/ios/RCTMarkdownStyle.h +++ b/ios/RCTMarkdownStyle.h @@ -18,10 +18,18 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) CGFloat codeFontSize; @property (nonatomic) UIColor *codeColor; @property (nonatomic) UIColor *codeBackgroundColor; +@property (nonatomic) UIColor *codeBorderColor; +@property (nonatomic) CGFloat codeBorderWidth; +@property (nonatomic) CGFloat codeBorderRadius; +@property (nonatomic) CGFloat codePadding; @property (nonatomic) NSString *preFontFamily; @property (nonatomic) CGFloat preFontSize; @property (nonatomic) UIColor *preColor; @property (nonatomic) UIColor *preBackgroundColor; +@property (nonatomic) UIColor *preBorderColor; +@property (nonatomic) CGFloat preBorderWidth; +@property (nonatomic) CGFloat preBorderRadius; +@property (nonatomic) CGFloat prePadding; @property (nonatomic) UIColor *mentionHereColor; @property (nonatomic) UIColor *mentionHereBackgroundColor; @property (nonatomic) UIColor *mentionUserColor; diff --git a/ios/RCTMarkdownStyle.mm b/ios/RCTMarkdownStyle.mm index c81b6ec3..9e82258f 100644 --- a/ios/RCTMarkdownStyle.mm +++ b/ios/RCTMarkdownStyle.mm @@ -30,11 +30,19 @@ - (instancetype)initWithStruct:(const facebook::react::MarkdownTextInputDecorato _codeFontSize = style.code.fontSize; _codeColor = RCTUIColorFromSharedColor(style.code.color); _codeBackgroundColor = RCTUIColorFromSharedColor(style.code.backgroundColor); + _codeBorderColor = RCTUIColorFromSharedColor(style.code.borderColor); + _codeBorderWidth = style.code.borderWidth; + _codeBorderRadius = style.code.borderRadius; + _codePadding = style.code.padding; _preFontFamily = RCTNSStringFromString(style.pre.fontFamily); _preFontSize = style.pre.fontSize; _preColor = RCTUIColorFromSharedColor(style.pre.color); _preBackgroundColor = RCTUIColorFromSharedColor(style.pre.backgroundColor); + _preBorderColor = RCTUIColorFromSharedColor(style.pre.borderColor); + _preBorderWidth = style.pre.borderWidth; + _preBorderRadius = style.pre.borderRadius; + _prePadding = style.pre.padding; _mentionHereColor = RCTUIColorFromSharedColor(style.mentionHere.color); _mentionHereBackgroundColor = RCTUIColorFromSharedColor(style.mentionHere.backgroundColor); @@ -68,11 +76,19 @@ - (instancetype)initWithDictionary:(NSDictionary *)json _codeFontSize = [RCTConvert CGFloat:json[@"code"][@"fontSize"]]; _codeColor = [RCTConvert UIColor:json[@"code"][@"color"]]; _codeBackgroundColor = [RCTConvert UIColor:json[@"code"][@"backgroundColor"]]; + _codeBorderColor = [RCTConvert UIColor:json[@"code"][@"borderColor"]]; + _codeBorderWidth = [RCTConvert CGFloat:json[@"code"][@"borderWidth"]]; + _codeBorderRadius = [RCTConvert CGFloat:json[@"code"][@"borderRadius"]]; + _codePadding = [RCTConvert CGFloat:json[@"code"][@"padding"]]; _preFontFamily = [RCTConvert NSString:json[@"pre"][@"fontFamily"]]; _preFontSize = [RCTConvert CGFloat:json[@"pre"][@"fontSize"]]; _preColor = [RCTConvert UIColor:json[@"pre"][@"color"]]; _preBackgroundColor = [RCTConvert UIColor:json[@"pre"][@"backgroundColor"]]; + _preBorderColor = [RCTConvert UIColor:json[@"pre"][@"borderColor"]]; + _preBorderWidth = [RCTConvert CGFloat:json[@"pre"][@"borderWidth"]]; + _preBorderRadius = [RCTConvert CGFloat:json[@"pre"][@"borderRadius"]]; + _prePadding = [RCTConvert CGFloat:json[@"pre"][@"padding"]]; _mentionHereColor = [RCTConvert UIColor:json[@"mentionHere"][@"color"]]; _mentionHereBackgroundColor = [RCTConvert UIColor:json[@"mentionHere"][@"backgroundColor"]]; diff --git a/ios/RCTMarkdownUtils.h b/ios/RCTMarkdownUtils.h index 4d080bb8..bb1ae2ad 100644 --- a/ios/RCTMarkdownUtils.h +++ b/ios/RCTMarkdownUtils.h @@ -7,6 +7,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) RCTMarkdownStyle *markdownStyle; @property (nonatomic) NSMutableArray *blockquoteRangesAndLevels; +@property (nonatomic) NSMutableArray *codeRanges; +@property (nonatomic) NSMutableArray *preRanges; - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withAttributes:(nullable NSDictionary*)attributes; diff --git a/ios/RCTMarkdownUtils.mm b/ios/RCTMarkdownUtils.mm index a5228843..79200681 100644 --- a/ios/RCTMarkdownUtils.mm +++ b/ios/RCTMarkdownUtils.mm @@ -47,6 +47,8 @@ - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withA [attributedString addAttribute:NSUnderlineStyleAttributeName value:[NSNumber numberWithInteger:NSUnderlineStyleNone] range:NSMakeRange(0, attributedString.length)]; _blockquoteRangesAndLevels = [NSMutableArray new]; + _codeRanges = [NSMutableArray new]; + _preRanges = [NSMutableArray new]; [ranges enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { NSDictionary *item = obj; @@ -100,7 +102,7 @@ - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withA [attributedString addAttribute:NSStrikethroughStyleAttributeName value:[NSNumber numberWithInteger:NSUnderlineStyleSingle] range:range]; } else if ([type isEqualToString:@"code"]) { [attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.codeColor range:range]; - [attributedString addAttribute:NSBackgroundColorAttributeName value:_markdownStyle.codeBackgroundColor range:range]; + [_codeRanges addObject:[NSValue valueWithRange:range]]; } else if ([type isEqualToString:@"mention-here"]) { [attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.mentionHereColor range:range]; [attributedString addAttribute:NSBackgroundColorAttributeName value:_markdownStyle.mentionHereBackgroundColor range:range]; @@ -122,10 +124,13 @@ - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withA @"depth": @(depth) }]; } else if ([type isEqualToString:@"pre"]) { + CGFloat indent = _markdownStyle.prePadding; + NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; + paragraphStyle.firstLineHeadIndent = indent; + paragraphStyle.headIndent = indent; + [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range]; [attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.preColor range:range]; - NSRange rangeForBackground = [inputString characterAtIndex:range.location] == '\n' ? NSMakeRange(range.location + 1, range.length - 1) : range; - [attributedString addAttribute:NSBackgroundColorAttributeName value:_markdownStyle.preBackgroundColor range:rangeForBackground]; - // TODO: pass background color and ranges to layout manager + [_preRanges addObject:[NSValue valueWithRange:range]]; } else if ([type isEqualToString:@"h1"]) { NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; NSRange rangeWithHashAndSpace = NSMakeRange(range.location - 2, range.length + 2); // we also need to include prepending "# " @@ -141,7 +146,6 @@ - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withA _prevMarkdownStyle = _markdownStyle; return attributedString; - } } diff --git a/src/MarkdownTextInputDecoratorViewNativeComponent.ts b/src/MarkdownTextInputDecoratorViewNativeComponent.ts index 729d183c..0c7d2338 100644 --- a/src/MarkdownTextInputDecoratorViewNativeComponent.ts +++ b/src/MarkdownTextInputDecoratorViewNativeComponent.ts @@ -27,12 +27,20 @@ interface MarkdownStyle { fontSize: Float; color: ColorValue; backgroundColor: ColorValue; + borderColor: ColorValue; + borderWidth: Float; + borderRadius: Float; + padding: Float; }; pre: { fontFamily: string; fontSize: Float; color: ColorValue; backgroundColor: ColorValue; + borderColor: ColorValue; + borderWidth: Float; + borderRadius: Float; + padding: Float; }; mentionHere: { color: ColorValue; diff --git a/src/styleUtils.ts b/src/styleUtils.ts index 2b76bded..9c9eeb2d 100644 --- a/src/styleUtils.ts +++ b/src/styleUtils.ts @@ -37,12 +37,20 @@ function makeDefaultMarkdownStyle(): MarkdownStyle { fontSize: 20, color: 'black', backgroundColor: 'lightgray', + borderColor: 'gray', + borderWidth: 1, + borderRadius: 4, + padding: 0, }, pre: { fontFamily: FONT_FAMILY_MONOSPACE, fontSize: 20, color: 'black', backgroundColor: 'lightgray', + borderColor: 'gray', + borderWidth: 1, + borderRadius: 4, + padding: 2, }, mentionHere: { color: 'green',