From 589feb1f5af697ea435667ea626511f09e2a47d5 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 | 139 +++++++++- ios/RCTMarkdownStyle.h | 8 + ios/RCTMarkdownStyle.mm | 16 ++ ios/RCTMarkdownUtils.h | 2 + ios/RCTMarkdownUtils.mm | 262 +++++++++--------- ...wnTextInputDecoratorViewNativeComponent.ts | 8 + src/styleUtils.ts | 8 + 7 files changed, 304 insertions(+), 139 deletions(-) diff --git a/ios/MarkdownLayoutManager.mm b/ios/MarkdownLayoutManager.mm index 3974ba98..918b69ae 100644 --- a/ios/MarkdownLayoutManager.mm +++ b/ios/MarkdownLayoutManager.mm @@ -2,20 +2,39 @@ @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]; + 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; - RCTMarkdownUtils *markdownUtils = [self valueForKey:@"markdownUtils"]; - [markdownUtils.blockquoteRangesAndLevels enumerateObjectsUsingBlock:^(NSDictionary *item, NSUInteger idx, BOOL * _Nonnull stop) { + + [ranges 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) { + if ([self isRange:range inRange:glyphRange]) { isBlockquote = YES; *stop = YES; } @@ -24,17 +43,117 @@ - (void)drawBackgroundForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origi 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; + CGFloat shift = margin + width + padding; for (int level = 0; level < currentDepth; level++) { - CGFloat x = paddingLeft + (level * shift) + markdownUtils.markdownStyle.blockquoteMarginLeft; + CGFloat x = paddingLeft + (level * shift) + margin; CGRect lineRect = CGRectMake(x, y, width, height); - [markdownUtils.markdownStyle.blockquoteBorderColor setFill]; + [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]; + }]; + }]; +} + +- (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 dc41a369..2be51b33 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 a2752cdf..4909c7b6 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); @@ -71,11 +79,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 879d5cac..f53e955c 100644 --- a/ios/RCTMarkdownUtils.mm +++ b/ios/RCTMarkdownUtils.mm @@ -13,139 +13,143 @@ @implementation RCTMarkdownUtils { - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withAttributes:(nullable NSDictionary *)attributes { - @synchronized (self) { - if (input == nil) { - return nil; - } - - NSString *inputString = [input string]; - if ([inputString isEqualToString:_prevInputString] && [attributes isEqualToDictionary:_prevTextAttributes] && [_markdownStyle isEqual:_prevMarkdownStyle]) { - return _prevAttributedString; - } - - static JSContext *ctx = nil; - static JSValue *function = nil; - if (ctx == nil) { - NSString *path = [[NSBundle mainBundle] pathForResource:@"react-native-live-markdown-parser" ofType:@"js"]; - assert(path != nil && "[react-native-live-markdown] Markdown parser bundle not found"); - NSString *content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL]; - assert(content != nil && "[react-native-live-markdown] Markdown parser bundle is empty"); - ctx = [[JSContext alloc] init]; - [ctx evaluateScript:content]; - function = ctx[@"parseExpensiMarkToRanges"]; - } - - JSValue *result = [function callWithArguments:@[inputString]]; - NSArray *ranges = [result toArray]; - - NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:inputString attributes:attributes]; - [attributedString beginEditing]; - - // If the attributed string ends with underlined text, blurring the single-line input imprints the underline style across the whole string. - // It looks like a bug in iOS, as there is no underline style to be found in the attributed string, especially after formatting. - // This is a workaround that applies the NSUnderlineStyleNone to the string before iterating over ranges which resolves this problem. - [attributedString addAttribute:NSUnderlineStyleAttributeName value:[NSNumber numberWithInteger:NSUnderlineStyleNone] range:NSMakeRange(0, attributedString.length)]; - - _blockquoteRangesAndLevels = [NSMutableArray new]; + @synchronized (self) { + if (input == nil) { + return nil; + } - [ranges enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - NSDictionary *item = obj; - NSString *type = [item valueForKey:@"type"]; - NSInteger location = [[item valueForKey:@"start"] unsignedIntegerValue]; - NSInteger length = [[item valueForKey:@"length"] unsignedIntegerValue]; - NSInteger depth = [[item valueForKey:@"depth"] unsignedIntegerValue] ?: 1; - NSRange range = NSMakeRange(location, length); + NSString *inputString = [input string]; + if ([inputString isEqualToString:_prevInputString] && [attributes isEqualToDictionary:_prevTextAttributes] && [_markdownStyle isEqual:_prevMarkdownStyle]) { + return _prevAttributedString; + } - if ([type isEqualToString:@"bold"] || [type isEqualToString:@"italic"] || [type isEqualToString:@"code"] || [type isEqualToString:@"pre"] || [type isEqualToString:@"h1"] || [type isEqualToString:@"emoji"]) { - UIFont *font = [attributedString attribute:NSFontAttributeName atIndex:location effectiveRange:NULL]; - if ([type isEqualToString:@"bold"]) { - font = [RCTFont updateFont:font withWeight:@"bold"]; - } else if ([type isEqualToString:@"italic"]) { - font = [RCTFont updateFont:font withStyle:@"italic"]; - } else if ([type isEqualToString:@"code"]) { - font = [RCTFont updateFont:font withFamily:_markdownStyle.codeFontFamily - size:[NSNumber numberWithFloat:_markdownStyle.codeFontSize] - weight:nil - style:nil - variant:nil - scaleMultiplier:0]; - } else if ([type isEqualToString:@"pre"]) { - font = [RCTFont updateFont:font withFamily:_markdownStyle.preFontFamily - size:[NSNumber numberWithFloat:_markdownStyle.preFontSize] - weight:nil - style:nil - variant:nil - scaleMultiplier:0]; - } else if ([type isEqualToString:@"h1"]) { - font = [RCTFont updateFont:font withFamily:nil - size:[NSNumber numberWithFloat:_markdownStyle.h1FontSize] - weight:@"bold" - style:nil - variant:nil - scaleMultiplier:0]; - } else if ([type isEqualToString:@"emoji"]) { - font = [RCTFont updateFont:font withFamily:nil - size:[NSNumber numberWithFloat:_markdownStyle.emojiFontSize] - weight:nil - style:nil - variant:nil - scaleMultiplier:0]; - } - [attributedString addAttribute:NSFontAttributeName value:font range:range]; - } + static JSContext *ctx = nil; + static JSValue *function = nil; + if (ctx == nil) { + NSString *path = [[NSBundle mainBundle] pathForResource:@"react-native-live-markdown-parser" ofType:@"js"]; + assert(path != nil && "[react-native-live-markdown] Markdown parser bundle not found"); + NSString *content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL]; + assert(content != nil && "[react-native-live-markdown] Markdown parser bundle is empty"); + ctx = [[JSContext alloc] init]; + [ctx evaluateScript:content]; + function = ctx[@"parseExpensiMarkToRanges"]; + } - if ([type isEqualToString:@"syntax"]) { - [attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.syntaxColor range:range]; - } else if ([type isEqualToString:@"strikethrough"]) { - [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]; - } else if ([type isEqualToString:@"mention-here"]) { - [attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.mentionHereColor range:range]; - [attributedString addAttribute:NSBackgroundColorAttributeName value:_markdownStyle.mentionHereBackgroundColor range:range]; - } else if ([type isEqualToString:@"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]; - } else if ([type isEqualToString:@"mention-report"]) { - [attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.mentionReportColor range:range]; - [attributedString addAttribute:NSBackgroundColorAttributeName value:_markdownStyle.mentionReportBackgroundColor range:range]; - } else if ([type isEqualToString:@"link"]) { - [attributedString addAttribute:NSUnderlineStyleAttributeName value:[NSNumber numberWithInteger:NSUnderlineStyleSingle] range:range]; - [attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.linkColor range:range]; - } else if ([type isEqualToString:@"blockquote"]) { - CGFloat indent = (_markdownStyle.blockquoteMarginLeft + _markdownStyle.blockquoteBorderWidth + _markdownStyle.blockquotePaddingLeft) * depth; - NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; - paragraphStyle.firstLineHeadIndent = indent; - paragraphStyle.headIndent = indent; - [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range]; - [_blockquoteRangesAndLevels addObject:@{ - @"range": [NSValue valueWithRange:range], - @"depth": @(depth) - }]; - } else if ([type isEqualToString:@"pre"]) { - [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 - } else if ([type isEqualToString:@"h1"]) { - NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; - NSRange rangeWithHashAndSpace = NSMakeRange(range.location - 2, range.length + 2); // we also need to include prepending "# " - [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:rangeWithHashAndSpace]; - } + JSValue *result = [function callWithArguments:@[inputString]]; + NSArray *ranges = [result toArray]; + + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:inputString attributes:attributes]; + [attributedString beginEditing]; + + // If the attributed string ends with underlined text, blurring the single-line input imprints the underline style across the whole string. + // It looks like a bug in iOS, as there is no underline style to be found in the attributed string, especially after formatting. + // This is a workaround that applies the NSUnderlineStyleNone to the string before iterating over ranges which resolves this problem. + [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; + NSString *type = [item valueForKey:@"type"]; + NSInteger location = [[item valueForKey:@"start"] unsignedIntegerValue]; + NSInteger length = [[item valueForKey:@"length"] unsignedIntegerValue]; + NSInteger depth = [[item valueForKey:@"depth"] unsignedIntegerValue] ?: 1; + NSRange range = NSMakeRange(location, length); + + if ([type isEqualToString:@"bold"] || [type isEqualToString:@"italic"] || [type isEqualToString:@"code"] || [type isEqualToString:@"pre"] || [type isEqualToString:@"h1"] || [type isEqualToString:@"emoji"]) { + UIFont *font = [attributedString attribute:NSFontAttributeName atIndex:location effectiveRange:NULL]; + if ([type isEqualToString:@"bold"]) { + font = [RCTFont updateFont:font withWeight:@"bold"]; + } else if ([type isEqualToString:@"italic"]) { + font = [RCTFont updateFont:font withStyle:@"italic"]; + } else if ([type isEqualToString:@"code"]) { + font = [RCTFont updateFont:font withFamily:_markdownStyle.codeFontFamily + size:[NSNumber numberWithFloat:_markdownStyle.codeFontSize] + weight:nil + style:nil + variant:nil + scaleMultiplier:0]; + } else if ([type isEqualToString:@"pre"]) { + font = [RCTFont updateFont:font withFamily:_markdownStyle.preFontFamily + size:[NSNumber numberWithFloat:_markdownStyle.preFontSize] + weight:nil + style:nil + variant:nil + scaleMultiplier:0]; + } else if ([type isEqualToString:@"h1"]) { + font = [RCTFont updateFont:font withFamily:nil + size:[NSNumber numberWithFloat:_markdownStyle.h1FontSize] + weight:@"bold" + style:nil + variant:nil + scaleMultiplier:0]; + } else if ([type isEqualToString:@"emoji"]) { + font = [RCTFont updateFont:font withFamily:nil + size:[NSNumber numberWithFloat:_markdownStyle.emojiFontSize] + weight:nil + style:nil + variant:nil + scaleMultiplier:0]; + } + [attributedString addAttribute:NSFontAttributeName value:font range:range]; + } + + if ([type isEqualToString:@"syntax"]) { + [attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.syntaxColor range:range]; + } else if ([type isEqualToString:@"strikethrough"]) { + [attributedString addAttribute:NSStrikethroughStyleAttributeName value:[NSNumber numberWithInteger:NSUnderlineStyleSingle] range:range]; + } else if ([type isEqualToString:@"code"]) { + [attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.codeColor 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]; + } else if ([type isEqualToString:@"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]; + } else if ([type isEqualToString:@"mention-report"]) { + [attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.mentionReportColor range:range]; + [attributedString addAttribute:NSBackgroundColorAttributeName value:_markdownStyle.mentionReportBackgroundColor range:range]; + } else if ([type isEqualToString:@"link"]) { + [attributedString addAttribute:NSUnderlineStyleAttributeName value:[NSNumber numberWithInteger:NSUnderlineStyleSingle] range:range]; + [attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.linkColor range:range]; + } else if ([type isEqualToString:@"blockquote"]) { + CGFloat indent = (_markdownStyle.blockquoteMarginLeft + _markdownStyle.blockquoteBorderWidth + _markdownStyle.blockquotePaddingLeft) * depth; + NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; + paragraphStyle.firstLineHeadIndent = indent; + paragraphStyle.headIndent = indent; + [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range]; + [_blockquoteRangesAndLevels addObject:@{ + @"range": [NSValue valueWithRange:range], + @"depth": @(depth) }]; - - [attributedString endEditing]; - - _prevInputString = inputString; - _prevAttributedString = attributedString; - _prevTextAttributes = attributes; - _prevMarkdownStyle = _markdownStyle; - - return attributedString; - - } + } 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]; + [_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 "# " + [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:rangeWithHashAndSpace]; + } + }]; + + [attributedString endEditing]; + + _prevInputString = inputString; + _prevAttributedString = attributedString; + _prevTextAttributes = attributes; + _prevMarkdownStyle = _markdownStyle; + + return attributedString; + } } @end diff --git a/src/MarkdownTextInputDecoratorViewNativeComponent.ts b/src/MarkdownTextInputDecoratorViewNativeComponent.ts index 3b79c7be..85893f73 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 a6c161ac..18ea7c09 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',