|
| 1 | +#import "MarkdownFormatter.h" |
| 2 | +#import <React/RCTFont.h> |
| 3 | + |
| 4 | +@implementation MarkdownFormatter |
| 5 | + |
| 6 | +- (nonnull NSAttributedString *)format:(nonnull NSString *)text |
| 7 | + withAttributes:(nullable NSDictionary<NSAttributedStringKey, id> *)attributes |
| 8 | + withMarkdownRanges:(nonnull NSArray<MarkdownRange *> *)markdownRanges |
| 9 | + withMarkdownStyle:(nonnull RCTMarkdownStyle *)markdownStyle |
| 10 | +{ |
| 11 | + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:text attributes:attributes]; |
| 12 | + |
| 13 | + [attributedString beginEditing]; |
| 14 | + |
| 15 | + // If the attributed string ends with underlined text, blurring the single-line input imprints the underline style across the whole string. |
| 16 | + // It looks like a bug in iOS, as there is no underline style to be found in the attributed string, especially after formatting. |
| 17 | + // This is a workaround that applies the NSUnderlineStyleNone to the string before iterating over ranges which resolves this problem. |
| 18 | + [attributedString addAttribute:NSUnderlineStyleAttributeName |
| 19 | + value:[NSNumber numberWithInteger:NSUnderlineStyleNone] |
| 20 | + range:NSMakeRange(0, attributedString.length)]; |
| 21 | + |
| 22 | + for (MarkdownRange *markdownRange in markdownRanges) { |
| 23 | + [self applyRangeToAttributedString:attributedString |
| 24 | + type:std::string([markdownRange.type UTF8String]) |
| 25 | + range:markdownRange.range |
| 26 | + depth:markdownRange.depth |
| 27 | + markdownStyle:markdownStyle]; |
| 28 | + } |
| 29 | + |
| 30 | + RCTApplyBaselineOffset(attributedString); |
| 31 | + |
| 32 | + [attributedString endEditing]; |
| 33 | + |
| 34 | + return attributedString; |
| 35 | +} |
| 36 | + |
| 37 | +- (void)applyRangeToAttributedString:(NSMutableAttributedString *)attributedString |
| 38 | + type:(const std::string)type |
| 39 | + range:(const NSRange)range |
| 40 | + depth:(const int)depth |
| 41 | + markdownStyle:(nonnull RCTMarkdownStyle *)markdownStyle { |
| 42 | + if (type == "bold" || type == "italic" || type == "code" || type == "pre" || type == "h1" || type == "emoji") { |
| 43 | + UIFont *font = [attributedString attribute:NSFontAttributeName atIndex:range.location effectiveRange:NULL]; |
| 44 | + if (type == "bold") { |
| 45 | + font = [RCTFont updateFont:font withWeight:@"bold"]; |
| 46 | + } else if (type == "italic") { |
| 47 | + font = [RCTFont updateFont:font withStyle:@"italic"]; |
| 48 | + } else if (type == "code") { |
| 49 | + font = [RCTFont updateFont:font withFamily:markdownStyle.codeFontFamily |
| 50 | + size:[NSNumber numberWithFloat:markdownStyle.codeFontSize] |
| 51 | + weight:nil |
| 52 | + style:nil |
| 53 | + variant:nil |
| 54 | + scaleMultiplier:0]; |
| 55 | + } else if (type == "pre") { |
| 56 | + font = [RCTFont updateFont:font withFamily:markdownStyle.preFontFamily |
| 57 | + size:[NSNumber numberWithFloat:markdownStyle.preFontSize] |
| 58 | + weight:nil |
| 59 | + style:nil |
| 60 | + variant:nil |
| 61 | + scaleMultiplier:0]; |
| 62 | + } else if (type == "h1") { |
| 63 | + font = [RCTFont updateFont:font withFamily:nil |
| 64 | + size:[NSNumber numberWithFloat:markdownStyle.h1FontSize] |
| 65 | + weight:@"bold" |
| 66 | + style:nil |
| 67 | + variant:nil |
| 68 | + scaleMultiplier:0]; |
| 69 | + } else if (type == "emoji") { |
| 70 | + font = [RCTFont updateFont:font withFamily:nil |
| 71 | + size:[NSNumber numberWithFloat:markdownStyle.emojiFontSize] |
| 72 | + weight:nil |
| 73 | + style:nil |
| 74 | + variant:nil |
| 75 | + scaleMultiplier:0]; |
| 76 | + } |
| 77 | + [attributedString addAttribute:NSFontAttributeName value:font range:range]; |
| 78 | + } |
| 79 | + |
| 80 | + if (type == "syntax") { |
| 81 | + [attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.syntaxColor range:range]; |
| 82 | + } else if (type == "strikethrough") { |
| 83 | + [attributedString addAttribute:NSStrikethroughStyleAttributeName value:[NSNumber numberWithInteger:NSUnderlineStyleSingle] range:range]; |
| 84 | + } else if (type == "code") { |
| 85 | + [attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.codeColor range:range]; |
| 86 | + [attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.codeBackgroundColor range:range]; |
| 87 | + } else if (type == "mention-here") { |
| 88 | + [attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.mentionHereColor range:range]; |
| 89 | + [attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.mentionHereBackgroundColor range:range]; |
| 90 | + } else if (type == "mention-user") { |
| 91 | + // TODO: change mention color when it mentions current user |
| 92 | + [attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.mentionUserColor range:range]; |
| 93 | + [attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.mentionUserBackgroundColor range:range]; |
| 94 | + } else if (type == "mention-report") { |
| 95 | + [attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.mentionReportColor range:range]; |
| 96 | + [attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.mentionReportBackgroundColor range:range]; |
| 97 | + } else if (type == "link") { |
| 98 | + [attributedString addAttribute:NSUnderlineStyleAttributeName value:[NSNumber numberWithInteger:NSUnderlineStyleSingle] range:range]; |
| 99 | + [attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.linkColor range:range]; |
| 100 | + } else if (type == "blockquote") { |
| 101 | + CGFloat indent = (markdownStyle.blockquoteMarginLeft + markdownStyle.blockquoteBorderWidth + markdownStyle.blockquotePaddingLeft) * depth; |
| 102 | + NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; |
| 103 | + paragraphStyle.firstLineHeadIndent = indent; |
| 104 | + paragraphStyle.headIndent = indent; |
| 105 | + [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range]; |
| 106 | + [attributedString addAttribute:RCTLiveMarkdownBlockquoteDepthAttributeName value:@(depth) range:range]; |
| 107 | + } else if (type == "pre") { |
| 108 | + [attributedString addAttribute:NSForegroundColorAttributeName value:markdownStyle.preColor range:range]; |
| 109 | + NSRange rangeForBackground = [[attributedString string] characterAtIndex:range.location] == '\n' ? NSMakeRange(range.location + 1, range.length - 1) : range; |
| 110 | + [attributedString addAttribute:NSBackgroundColorAttributeName value:markdownStyle.preBackgroundColor range:rangeForBackground]; |
| 111 | + // TODO: pass background color and ranges to layout manager |
| 112 | + } |
| 113 | +} |
| 114 | + |
| 115 | +static void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText) |
| 116 | +{ |
| 117 | + __block CGFloat maximumLineHeight = 0; |
| 118 | + |
| 119 | + [attributedText enumerateAttribute:NSParagraphStyleAttributeName |
| 120 | + inRange:NSMakeRange(0, attributedText.length) |
| 121 | + options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired |
| 122 | + usingBlock:^(NSParagraphStyle *paragraphStyle, __unused NSRange range, __unused BOOL *stop) { |
| 123 | + if (!paragraphStyle) { |
| 124 | + return; |
| 125 | + } |
| 126 | + |
| 127 | + maximumLineHeight = MAX(paragraphStyle.maximumLineHeight, maximumLineHeight); |
| 128 | + }]; |
| 129 | + |
| 130 | + if (maximumLineHeight == 0) { |
| 131 | + // `lineHeight` was not specified, nothing to do. |
| 132 | + return; |
| 133 | + } |
| 134 | + |
| 135 | + __block CGFloat maximumFontLineHeight = 0; |
| 136 | + |
| 137 | + [attributedText enumerateAttribute:NSFontAttributeName |
| 138 | + inRange:NSMakeRange(0, attributedText.length) |
| 139 | + options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired |
| 140 | + usingBlock:^(UIFont *font, NSRange range, __unused BOOL *stop) { |
| 141 | + if (!font) { |
| 142 | + return; |
| 143 | + } |
| 144 | + |
| 145 | + maximumFontLineHeight = MAX(font.lineHeight, maximumFontLineHeight); |
| 146 | + }]; |
| 147 | + |
| 148 | + if (maximumLineHeight < maximumFontLineHeight) { |
| 149 | + return; |
| 150 | + } |
| 151 | + |
| 152 | + CGFloat baseLineOffset = (maximumLineHeight - maximumFontLineHeight) / 2.0; |
| 153 | + [attributedText addAttribute:NSBaselineOffsetAttributeName |
| 154 | + value:@(baseLineOffset) |
| 155 | + range:NSMakeRange(0, attributedText.length)]; |
| 156 | +} |
| 157 | + |
| 158 | +@end |
0 commit comments