diff --git a/ios/MarkdownTextInputDecoratorView.mm b/ios/MarkdownTextInputDecoratorView.mm index eaa8a59bb..80a651b1f 100644 --- a/ios/MarkdownTextInputDecoratorView.mm +++ b/ios/MarkdownTextInputDecoratorView.mm @@ -3,7 +3,6 @@ #import #import -#import #import #ifdef RCT_NEW_ARCH_ENABLED @@ -23,7 +22,6 @@ @implementation MarkdownTextInputDecoratorView { __weak RCTBaseTextInputView *_textInput; #endif /* RCT_NEW_ARCH_ENABLED */ __weak UIView *_backedTextInputView; - __weak RCTBackedTextFieldDelegateAdapter *_adapter; __weak RCTUITextView *_textView; } @@ -64,18 +62,14 @@ - (void)didMoveToWindow { [_markdownUtils setMarkdownStyle:_markdownStyle]; [_textInput setMarkdownUtils:_markdownUtils]; - if ([_backedTextInputView isKindOfClass:[RCTUITextField class]]) { - RCTUITextField *textField = (RCTUITextField *)_backedTextInputView; - _adapter = [textField valueForKey:@"textInputDelegateAdapter"]; - [_adapter setMarkdownUtils:_markdownUtils]; - } else if ([_backedTextInputView isKindOfClass:[RCTUITextView class]]) { + if ([_backedTextInputView isKindOfClass:[RCTUITextView class]]) { _textView = (RCTUITextView *)_backedTextInputView; [_textView setMarkdownUtils:_markdownUtils]; NSLayoutManager *layoutManager = _textView.layoutManager; // switching to TextKit 1 compatibility mode layoutManager.allowsNonContiguousLayout = NO; // workaround for onScroll issue object_setClass(layoutManager, [MarkdownLayoutManager class]); [layoutManager setValue:_markdownUtils forKey:@"markdownUtils"]; - } else { + } else if (![_backedTextInputView isKindOfClass:[RCTUITextField class]]) { react_native_assert(false && "Cannot enable Markdown for this type of TextInput."); } } @@ -85,9 +79,6 @@ - (void)willMoveToWindow:(UIWindow *)newWindow if (_textInput != nil) { [_textInput setMarkdownUtils:nil]; } - if (_adapter != nil) { - [_adapter setMarkdownUtils:nil]; - } if (_textView != nil) { [_textView setMarkdownUtils:nil]; if (_textView.layoutManager != nil && [object_getClass(_textView.layoutManager) isEqual:[MarkdownLayoutManager class]]) { @@ -103,11 +94,15 @@ - (void)setMarkdownStyle:(RCTMarkdownStyle *)markdownStyle [_markdownUtils setMarkdownStyle:markdownStyle]; // apply new styles + if (_textView != nil) { + [_textView performSelector:@selector(textDidChange)]; + } else { #ifdef RCT_NEW_ARCH_ENABLED - [_textInput _setAttributedString:_backedTextInputView.attributedText]; + [_textInput _setAttributedString:_backedTextInputView.attributedText]; #else - [_textInput setAttributedText:_textInput.attributedText]; + [_textInput setAttributedText:_textInput.attributedText]; #endif /* RCT_NEW_ARCH_ENABLED */ + } } @end diff --git a/ios/RCTBackedTextFieldDelegateAdapter+Markdown.h b/ios/RCTBackedTextFieldDelegateAdapter+Markdown.h deleted file mode 100644 index f8ddc1d2a..000000000 --- a/ios/RCTBackedTextFieldDelegateAdapter+Markdown.h +++ /dev/null @@ -1,14 +0,0 @@ -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface RCTBackedTextFieldDelegateAdapter (Markdown) - -@property(nonatomic, nullable, getter=getMarkdownUtils) RCTMarkdownUtils *markdownUtils; - -- (void)markdown_textFieldDidChange; - -@end - -NS_ASSUME_NONNULL_END diff --git a/ios/RCTBackedTextFieldDelegateAdapter+Markdown.mm b/ios/RCTBackedTextFieldDelegateAdapter+Markdown.mm deleted file mode 100644 index 11c3baf8b..000000000 --- a/ios/RCTBackedTextFieldDelegateAdapter+Markdown.mm +++ /dev/null @@ -1,43 +0,0 @@ -#import -#import -#import -#import - -@implementation RCTBackedTextFieldDelegateAdapter (Markdown) - -- (void)setMarkdownUtils:(RCTMarkdownUtils *)markdownUtils { - objc_setAssociatedObject(self, @selector(getMarkdownUtils), markdownUtils, OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -- (RCTMarkdownUtils *)getMarkdownUtils { - return objc_getAssociatedObject(self, @selector(getMarkdownUtils)); -} - -- (void)markdown_textFieldDidChange -{ - RCTMarkdownUtils *markdownUtils = [self getMarkdownUtils]; - if (markdownUtils != nil) { - RCTUITextField *backedTextInputView = [self valueForKey:@"_backedTextInputView"]; - UITextRange *range = backedTextInputView.selectedTextRange; - backedTextInputView.attributedText = [markdownUtils parseMarkdown:backedTextInputView.attributedText withAttributes:backedTextInputView.defaultTextAttributes]; - [backedTextInputView setSelectedTextRange:range notifyDelegate:YES]; - } - - // Call the original method - [self markdown_textFieldDidChange]; -} - -+ (void)load -{ - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - Class cls = [self class]; - SEL originalSelector = @selector(textFieldDidChange); - SEL swizzledSelector = @selector(markdown_textFieldDidChange); - Method originalMethod = class_getInstanceMethod(cls, originalSelector); - Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector); - method_exchangeImplementations(originalMethod, swizzledMethod); - }); -} - -@end diff --git a/ios/RCTBaseTextInputView+Markdown.h b/ios/RCTBaseTextInputView+Markdown.h index 3d37adb25..6dfec6ede 100644 --- a/ios/RCTBaseTextInputView+Markdown.h +++ b/ios/RCTBaseTextInputView+Markdown.h @@ -1,8 +1,15 @@ +// This guard prevent this file to be compiled in the new architecture. +#ifndef RCT_NEW_ARCH_ENABLED + #import #import NS_ASSUME_NONNULL_BEGIN +@interface RCTBaseTextInputView (Private) +- (BOOL)textOf:(NSAttributedString *)newText equals:(NSAttributedString *)oldText; +@end + @interface RCTBaseTextInputView (Markdown) @property(nonatomic, nullable, getter=getMarkdownUtils) RCTMarkdownUtils *markdownUtils; @@ -11,8 +18,8 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)markdown_textOf:(NSAttributedString *)newText equals:(NSAttributedString *)oldText; -- (void)markdown_updateLocalData; - @end NS_ASSUME_NONNULL_END + +#endif /* RCT_NEW_ARCH_ENABLED */ diff --git a/ios/RCTBaseTextInputView+Markdown.mm b/ios/RCTBaseTextInputView+Markdown.mm index 209dc6bb3..525e94195 100644 --- a/ios/RCTBaseTextInputView+Markdown.mm +++ b/ios/RCTBaseTextInputView+Markdown.mm @@ -1,3 +1,8 @@ +// This guard prevent this file to be compiled in the new architecture. +#ifndef RCT_NEW_ARCH_ENABLED + +#import +#import #import #import #import @@ -14,9 +19,11 @@ - (RCTMarkdownUtils *)getMarkdownUtils { - (void)markdown_setAttributedText:(NSAttributedString *)attributedText { - RCTMarkdownUtils *markdownUtils = [self getMarkdownUtils]; - if (markdownUtils != nil) { - attributedText = [markdownUtils parseMarkdown:attributedText withAttributes:self.backedTextInputView.defaultTextAttributes]; + if (![self.backedTextInputView isKindOfClass:[RCTUITextView class]]) { + RCTMarkdownUtils *markdownUtils = [self getMarkdownUtils]; + if (markdownUtils != nil) { + attributedText = [markdownUtils parseMarkdown:attributedText withAttributes:self.backedTextInputView.defaultTextAttributes]; + } } // Call the original method @@ -33,29 +40,6 @@ - (BOOL)markdown_textOf:(NSAttributedString *)newText equals:(NSAttributedString return [self markdown_textOf:newText equals:oldText]; } -- (void)markdown_updateLocalData -{ - RCTMarkdownUtils *markdownUtils = [self getMarkdownUtils]; - if (markdownUtils != nil) { - id backedTextInputView = self.backedTextInputView; - NSAttributedString *oldAttributedText = backedTextInputView.attributedText; - NSAttributedString *newAttributedText = [markdownUtils parseMarkdown:oldAttributedText withAttributes:backedTextInputView.defaultTextAttributes]; - UITextRange *range = backedTextInputView.selectedTextRange; - - // update attributed text without emitting onSelectionChange event - id delegate = backedTextInputView.textInputDelegate; - backedTextInputView.textInputDelegate = nil; - [backedTextInputView setAttributedText:newAttributedText]; - backedTextInputView.textInputDelegate = delegate; - - // restore original selection and emit onSelectionChange event - [backedTextInputView setSelectedTextRange:range notifyDelegate:YES]; - } - - // Call the original method - [self markdown_updateLocalData]; -} - + (void)load { static dispatch_once_t onceToken; @@ -71,15 +55,6 @@ + (void)load method_exchangeImplementations(originalMethod, swizzledMethod); } - { - // swizzle updateLocalData - SEL originalSelector = @selector(updateLocalData); - SEL swizzledSelector = @selector(markdown_updateLocalData); - Method originalMethod = class_getInstanceMethod(cls, originalSelector); - Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector); - method_exchangeImplementations(originalMethod, swizzledMethod); - } - { // swizzle textOf SEL originalSelector = @selector(textOf:equals:); @@ -92,3 +67,5 @@ + (void)load } @end + +#endif /* RCT_NEW_ARCH_ENABLED */ diff --git a/ios/RCTMarkdownUtils.h b/ios/RCTMarkdownUtils.h index 4d080bb8f..2ac76a8b3 100644 --- a/ios/RCTMarkdownUtils.h +++ b/ios/RCTMarkdownUtils.h @@ -9,6 +9,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) NSMutableArray *blockquoteRangesAndLevels; - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withAttributes:(nullable NSDictionary*)attributes; +- (void)parseMarkdown:(nullable NSMutableAttributedString *)attributedString; @end diff --git a/ios/RCTMarkdownUtils.mm b/ios/RCTMarkdownUtils.mm index f188429a2..cb0fbfe69 100644 --- a/ios/RCTMarkdownUtils.mm +++ b/ios/RCTMarkdownUtils.mm @@ -11,143 +11,143 @@ @implementation RCTMarkdownUtils { __weak RCTMarkdownStyle *_prevMarkdownStyle; } -- (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withAttributes:(nullable NSDictionary *)attributes +- (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]; - - [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]; - [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]; - } - }]; + NSString *inputString = [input string]; + if ([inputString isEqualToString:_prevInputString] && [attributes isEqualToDictionary:_prevTextAttributes] && [_markdownStyle isEqual:_prevMarkdownStyle]) { + return _prevAttributedString; + } - RCTApplyBaselineOffset(attributedString); + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:inputString attributes: attributes]; + [self parseMarkdown:attributedString]; - [attributedString endEditing]; + _prevInputString = inputString; + _prevAttributedString = attributedString; + _prevTextAttributes = attributes; + _prevMarkdownStyle = _markdownStyle; - _prevInputString = inputString; - _prevAttributedString = attributedString; - _prevTextAttributes = attributes; - _prevMarkdownStyle = _markdownStyle; + return attributedString; +} - return attributedString; +- (void)parseMarkdown:(nullable NSMutableAttributedString *)attributedString +{ + @synchronized (self) { + if (attributedString == nil) { + return; + } + 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"]; } + + NSString *inputString = [attributedString string]; + JSValue *result = [function callWithArguments:@[inputString]]; + NSArray *ranges = [result toArray]; + + [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]; + + [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 = [[font copy] fontWithSize:_markdownStyle.emojiFontSize]; + } + [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]; + [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]; + } + }]; + + RCTApplyBaselineOffset(attributedString); + + [attributedString endEditing]; + } } static void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText) diff --git a/ios/RCTTextInputComponentView+Markdown.h b/ios/RCTTextInputComponentView+Markdown.h index 346bc7115..bb203d8d5 100644 --- a/ios/RCTTextInputComponentView+Markdown.h +++ b/ios/RCTTextInputComponentView+Markdown.h @@ -6,6 +6,11 @@ NS_ASSUME_NONNULL_BEGIN +@interface RCTTextInputComponentView (Private) +- (void)_setAttributedString:(NSAttributedString *)attributedString; +- (BOOL)_textOf:(NSAttributedString *)newText equals:(NSAttributedString *)oldText; +@end + @interface RCTTextInputComponentView (Markdown) @property(nonatomic, nullable, getter=getMarkdownUtils) RCTMarkdownUtils *markdownUtils; @@ -14,8 +19,6 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)markdown__textOf:(NSAttributedString *)newText equals:(NSAttributedString *)oldText; -- (void)_setAttributedString:(NSAttributedString *)attributedString; - @end NS_ASSUME_NONNULL_END diff --git a/ios/RCTTextInputComponentView+Markdown.mm b/ios/RCTTextInputComponentView+Markdown.mm index 1a4581f3e..0f5688c43 100644 --- a/ios/RCTTextInputComponentView+Markdown.mm +++ b/ios/RCTTextInputComponentView+Markdown.mm @@ -4,6 +4,7 @@ #import #import #import +#import #import #import "MarkdownShadowFamilyRegistry.h" @@ -15,7 +16,7 @@ - (void)setMarkdownUtils:(RCTMarkdownUtils *)markdownUtils { if (markdownUtils != nil) { // force Markdown formatting on first render because `_setAttributedText` is called before `setMarkdownUtils` - RCTUITextField *backedTextInputView = [self getBackedTextInputView]; + auto backedTextInputView = [self getBackedTextInputView]; backedTextInputView.attributedText = [markdownUtils parseMarkdown:backedTextInputView.attributedText withAttributes:backedTextInputView.defaultTextAttributes]; } } @@ -24,15 +25,14 @@ - (RCTMarkdownUtils *)getMarkdownUtils { return objc_getAssociatedObject(self, @selector(getMarkdownUtils)); } -- (RCTUITextField *)getBackedTextInputView { - RCTUITextField *backedTextInputView = [self valueForKey:@"_backedTextInputView"]; - return backedTextInputView; +- (id)getBackedTextInputView { + return [self valueForKey:@"_backedTextInputView"]; } - (void)markdown__setAttributedString:(NSAttributedString *)attributedString { - RCTMarkdownUtils *markdownUtils = [self getMarkdownUtils]; - RCTUITextField *backedTextInputView = [self getBackedTextInputView]; + auto markdownUtils = [self getMarkdownUtils]; + auto backedTextInputView = [self getBackedTextInputView]; if (markdownUtils != nil && backedTextInputView != nil) { attributedString = [markdownUtils parseMarkdown:attributedString withAttributes:backedTextInputView.defaultTextAttributes]; } else { diff --git a/ios/RCTUITextView+Markdown.h b/ios/RCTUITextView+Markdown.h index f792140ee..640efbd56 100644 --- a/ios/RCTUITextView+Markdown.h +++ b/ios/RCTUITextView+Markdown.h @@ -4,6 +4,11 @@ NS_ASSUME_NONNULL_BEGIN +@interface RCTUITextView (Private) +- (void)textDidChange; +- (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BOOL)notifyDelegate; +@end + @interface RCTUITextView (Markdown) @property(nonatomic, nullable, getter=getMarkdownUtils) RCTMarkdownUtils *markdownUtils; diff --git a/ios/RCTUITextView+Markdown.mm b/ios/RCTUITextView+Markdown.mm index 70f2d8820..54061b384 100644 --- a/ios/RCTUITextView+Markdown.mm +++ b/ios/RCTUITextView+Markdown.mm @@ -14,28 +14,54 @@ - (RCTMarkdownUtils *)getMarkdownUtils { - (void)markdown_textDidChange { - RCTMarkdownUtils *markdownUtils = [self getMarkdownUtils]; + auto markdownUtils = [self getMarkdownUtils]; if (markdownUtils != nil) { - UITextRange *range = self.selectedTextRange; - super.attributedText = [markdownUtils parseMarkdown:self.attributedText withAttributes:self.defaultTextAttributes]; - [super setSelectedTextRange:range]; // prevents cursor from jumping at the end when typing in the middle of the text - self.typingAttributes = self.defaultTextAttributes; // removes indent in new line when typing after blockquote + [markdownUtils parseMarkdown:self.textStorage]; } // Call the original method [self markdown_textDidChange]; } +- (int)offsetFromTextPosition:(UITextPosition *)textPosition +{ + return [self offsetFromPosition:self.beginningOfDocument toPosition:textPosition]; +} + +- (void)markdown_setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BOOL)notifyDelegate +{ +#ifdef RCT_NEW_ARCH_ENABLED + // Workaround for `Invalid parameter not satisfying: pos` crash on Fabric when selecting all text and replacing it with single character. + if ([self offsetFromTextPosition:selectedTextRange.start] <= 0 && [self offsetFromTextPosition:selectedTextRange.end] <= 0) { + return; + } +#endif /* RCT_NEW_ARCH_ENABLED */ + + // Call the original method + [self markdown_setSelectedTextRange:selectedTextRange notifyDelegate:notifyDelegate]; +} + + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class cls = [self class]; - SEL originalSelector = @selector(textDidChange); - SEL swizzledSelector = @selector(markdown_textDidChange); - Method originalMethod = class_getInstanceMethod(cls, originalSelector); - Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector); - method_exchangeImplementations(originalMethod, swizzledMethod); + + { + SEL originalSelector = @selector(textDidChange); + SEL swizzledSelector = @selector(markdown_textDidChange); + Method originalMethod = class_getInstanceMethod(cls, originalSelector); + Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector); + method_exchangeImplementations(originalMethod, swizzledMethod); + } + + { + SEL originalSelector = @selector(setSelectedTextRange:notifyDelegate:); + SEL swizzledSelector = @selector(markdown_setSelectedTextRange:notifyDelegate:); + Method originalMethod = class_getInstanceMethod(cls, originalSelector); + Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector); + method_exchangeImplementations(originalMethod, swizzledMethod); + } }); }