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 75a27f16..2a4f3c78 100644 --- a/ios/RCTMarkdownStyle.h +++ b/ios/RCTMarkdownStyle.h @@ -20,10 +20,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 ebc4311e..e5147657 100644 --- a/ios/RCTMarkdownUtils.mm +++ b/ios/RCTMarkdownUtils.mm @@ -17,151 +17,155 @@ @implementation RCTMarkdownUtils { - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withAttributes:(nullable NSDictionary *)attributes { - @synchronized (self) { - if (input == nil) { - return nil; - } + @synchronized (self) { + if (input == nil) { + return nil; + } - NSString *inputString = [input string]; - if ([inputString isEqualToString:_prevInputString] && [attributes isEqualToDictionary:_prevTextAttributes] && [_markdownStyle isEqual:_prevMarkdownStyle]) { - return _prevAttributedString; - } + NSString *inputString = [input string]; + if ([inputString isEqualToString:_prevInputString] && [attributes isEqualToDictionary:_prevTextAttributes] && [_markdownStyle isEqual:_prevMarkdownStyle]) { + return _prevAttributedString; + } - static std::shared_ptr runtime; - static std::mutex runtimeMutex; - auto lock = std::lock_guard(runtimeMutex); - - if (runtime == nullptr) { - 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"); - runtime = facebook::hermes::makeHermesRuntime(); - auto codeBuffer = std::make_shared([content UTF8String]); - runtime->evaluateJavaScript(codeBuffer, "evaluateJavaScript"); - } + static std::shared_ptr runtime; + static std::mutex runtimeMutex; + auto lock = std::lock_guard(runtimeMutex); + + if (runtime == nullptr) { + 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"); + runtime = facebook::hermes::makeHermesRuntime(); + auto codeBuffer = std::make_shared([content UTF8String]); + runtime->evaluateJavaScript(codeBuffer, "evaluateJavaScript"); + } - jsi::Runtime &rt = *runtime; - auto text = jsi::String::createFromUtf8(rt, [inputString UTF8String]); + jsi::Runtime &rt = *runtime; + auto text = jsi::String::createFromUtf8(rt, [inputString UTF8String]); - auto func = rt.global().getPropertyAsFunction(rt, "parseExpensiMarkToRanges"); - auto output = func.call(rt, text); - if (output.isUndefined()) { - return input; - } - const auto &ranges = output.asObject(rt).asArray(rt); - - 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]; - - for (size_t i = 0, n = ranges.size(rt); i < n; ++i) { - const auto &item = ranges.getValueAtIndex(rt, i).asObject(rt); - const auto &type = item.getProperty(rt, "type").asString(rt).utf8(rt); - const auto &location = static_cast(item.getProperty(rt, "start").asNumber()); - const auto &length = static_cast(item.getProperty(rt, "length").asNumber()); - const auto &depth = item.hasProperty(rt, "depth") ? static_cast(item.getProperty(rt, "depth").asNumber()) : 1; - - if (length == 0 || location + length > attributedString.length) { - continue; - } - - NSRange range = NSMakeRange(location, length); - - if (type == "bold" || type == "italic" || type == "code" || type == "pre" || type == "h1" || type == "emoji") { - UIFont *font = [attributedString attribute:NSFontAttributeName atIndex:location effectiveRange:NULL]; - if (type == "bold") { - font = [RCTFont updateFont:font withWeight:@"bold"]; - } else if (type == "italic") { - font = [RCTFont updateFont:font withStyle:@"italic"]; - } else if (type == "code") { - font = [RCTFont updateFont:font withFamily:_markdownStyle.codeFontFamily - size:[NSNumber numberWithFloat:_markdownStyle.codeFontSize] - weight:nil - style:nil - variant:nil - scaleMultiplier:0]; - } else if (type == "pre") { - font = [RCTFont updateFont:font withFamily:_markdownStyle.preFontFamily - size:[NSNumber numberWithFloat:_markdownStyle.preFontSize] - weight:nil - style:nil - variant:nil - scaleMultiplier:0]; - } else if (type == "h1") { - font = [RCTFont updateFont:font withFamily:nil - size:[NSNumber numberWithFloat:_markdownStyle.h1FontSize] - weight:@"bold" - style:nil - variant:nil - scaleMultiplier:0]; - } else if (type == "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 == "syntax") { - [attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.syntaxColor range:range]; - } else if (type == "strikethrough") { - [attributedString addAttribute:NSStrikethroughStyleAttributeName value:[NSNumber numberWithInteger:NSUnderlineStyleSingle] range:range]; - } else if (type == "code") { - [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]; - [attributedString addAttribute:NSBackgroundColorAttributeName value:_markdownStyle.mentionHereBackgroundColor range:range]; - } else if (type == "mention-user") { - // TODO: change mention color when it mentions current user - [attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.mentionUserColor range:range]; - [attributedString addAttribute:NSBackgroundColorAttributeName value:_markdownStyle.mentionUserBackgroundColor range:range]; - } else if (type == "mention-report") { - [attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.mentionReportColor range:range]; - [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]; - } else if (type == "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 == "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 - } + auto func = rt.global().getPropertyAsFunction(rt, "parseExpensiMarkToRanges"); + auto output = func.call(rt, text); + if (output.isUndefined()) { + return input; + } + const auto &ranges = output.asObject(rt).asArray(rt); + + 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]; + + for (size_t i = 0, n = ranges.size(rt); i < n; ++i) { + const auto &item = ranges.getValueAtIndex(rt, i).asObject(rt); + const auto &type = item.getProperty(rt, "type").asString(rt).utf8(rt); + const auto &location = static_cast(item.getProperty(rt, "start").asNumber()); + const auto &length = static_cast(item.getProperty(rt, "length").asNumber()); + const auto &depth = item.hasProperty(rt, "depth") ? static_cast(item.getProperty(rt, "depth").asNumber()) : 1; + + if (length == 0 || location + length > attributedString.length) { + continue; + } + + NSRange range = NSMakeRange(location, length); + + if (type == "bold" || type == "italic" || type == "code" || type == "pre" || type == "h1" || type == "emoji") { + UIFont *font = [attributedString attribute:NSFontAttributeName atIndex:location effectiveRange:NULL]; + if (type == "bold") { + font = [RCTFont updateFont:font withWeight:@"bold"]; + } else if (type == "italic") { + font = [RCTFont updateFont:font withStyle:@"italic"]; + } else if (type == "code") { + font = [RCTFont updateFont:font withFamily:_markdownStyle.codeFontFamily + size:[NSNumber numberWithFloat:_markdownStyle.codeFontSize] + weight:nil + style:nil + variant:nil + scaleMultiplier:0]; + } else if (type == "pre") { + font = [RCTFont updateFont:font withFamily:_markdownStyle.preFontFamily + size:[NSNumber numberWithFloat:_markdownStyle.preFontSize] + weight:nil + style:nil + variant:nil + scaleMultiplier:0]; + } else if (type == "h1") { + font = [RCTFont updateFont:font withFamily:nil + size:[NSNumber numberWithFloat:_markdownStyle.h1FontSize] + weight:@"bold" + style:nil + variant:nil + scaleMultiplier:0]; + } else if (type == "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 == "syntax") { + [attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.syntaxColor range:range]; + } else if (type == "strikethrough") { + [attributedString addAttribute:NSStrikethroughStyleAttributeName value:[NSNumber numberWithInteger:NSUnderlineStyleSingle] range:range]; + } else if (type == "code") { + [attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.codeColor range:range]; + [_codeRanges addObject:[NSValue valueWithRange:range]]; + } else if (type == "mention-here") { + [attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.mentionHereColor range:range]; + [attributedString addAttribute:NSBackgroundColorAttributeName value:_markdownStyle.mentionHereBackgroundColor range:range]; + } else if (type == "mention-user") { + // TODO: change mention color when it mentions current user + [attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.mentionUserColor range:range]; + [attributedString addAttribute:NSBackgroundColorAttributeName value:_markdownStyle.mentionUserBackgroundColor range:range]; + } else if (type == "mention-report") { + [attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.mentionReportColor range:range]; + [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]; + } else if (type == "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 == "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]]; + } + } - RCTApplyBaselineOffset(attributedString); - - [attributedString endEditing]; + RCTApplyBaselineOffset(attributedString); - _prevInputString = inputString; - _prevAttributedString = attributedString; - _prevTextAttributes = attributes; - _prevMarkdownStyle = _markdownStyle; + [attributedString endEditing]; - return attributedString; + _prevInputString = inputString; + _prevAttributedString = attributedString; + _prevTextAttributes = attributes; + _prevMarkdownStyle = _markdownStyle; - } + return attributedString; + } } static void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText) 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 4e66dcb8..9a19f6f5 100644 --- a/src/styleUtils.ts +++ b/src/styleUtils.ts @@ -35,12 +35,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',