diff --git a/apple/MarkdownBackedTextInputDelegate.h b/apple/MarkdownBackedTextInputDelegate.h new file mode 100644 index 000000000..ba1b3aca2 --- /dev/null +++ b/apple/MarkdownBackedTextInputDelegate.h @@ -0,0 +1,12 @@ +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface MarkdownBackedTextInputDelegate : NSObject + +- (instancetype)initWithTextView:(RCTUITextView *)textView; + +@end + +NS_ASSUME_NONNULL_END diff --git a/apple/MarkdownBackedTextInputDelegate.mm b/apple/MarkdownBackedTextInputDelegate.mm new file mode 100644 index 000000000..39803cc69 --- /dev/null +++ b/apple/MarkdownBackedTextInputDelegate.mm @@ -0,0 +1,75 @@ +#import "MarkdownBackedTextInputDelegate.h" + +@implementation MarkdownBackedTextInputDelegate { + __weak RCTUITextView *_textView; + id _originalTextInputDelegate; +} + +- (instancetype)initWithTextView:(RCTUITextView *)textView +{ + if (self = [super init]) { + _textView = textView; + _originalTextInputDelegate = _textView.textInputDelegate; + _textView.textInputDelegate = self; + } + return self; +} + +- (void)dealloc +{ + // Restore original text input delegate + _textView.textInputDelegate = _originalTextInputDelegate; +} + +- (void)textInputDidChange +{ + // After adding a newline at the end of the blockquote, the typing attributes in the new line + // still contain NSParagraphStyle with non-zero firstLineHeadIndent and headIntent. + // This causes the cursor to be shifted to the right instead of being located at the beginning of the line. + // Also, if the previous line of the text ends with a link, there will be underline blinks visible while typing. + // The following code resets typing attributes with default text attributes to fix both problems at once. + _textView.typingAttributes = _textView.defaultTextAttributes; + + // Delegate the call to the original text input delegate + [_originalTextInputDelegate textInputDidChange]; +} + +// Delegate all remaining calls to the original text input delegate + +- (void)textInputDidBeginEditing { + [_originalTextInputDelegate textInputDidBeginEditing]; +} + +- (void)textInputDidChangeSelection { + [_originalTextInputDelegate textInputDidChangeSelection]; +} + +- (void)textInputDidEndEditing { + [_originalTextInputDelegate textInputDidEndEditing]; +} + +- (void)textInputDidReturn { + [_originalTextInputDelegate textInputDidReturn]; +} + +- (BOOL)textInputShouldBeginEditing { + return [_originalTextInputDelegate textInputShouldBeginEditing]; +} + +- (nonnull NSString *)textInputShouldChangeText:(nonnull NSString *)text inRange:(NSRange)range { + return [_originalTextInputDelegate textInputShouldChangeText:text inRange:range]; +} + +- (BOOL)textInputShouldEndEditing { + return [_originalTextInputDelegate textInputShouldEndEditing]; +} + +- (BOOL)textInputShouldReturn { + return [_originalTextInputDelegate textInputShouldReturn]; +} + +- (BOOL)textInputShouldSubmitOnReturn { + return [_originalTextInputDelegate textInputShouldSubmitOnReturn]; +} + +@end diff --git a/apple/MarkdownCommitHook.mm b/apple/MarkdownCommitHook.mm index 9ff5be224..aad9d45c1 100644 --- a/apple/MarkdownCommitHook.mm +++ b/apple/MarkdownCommitHook.mm @@ -197,8 +197,8 @@ } // apply markdown - auto newString = [usedUtils parseMarkdown:nsAttributedString - withDefaultTextAttributes:defaultNSTextAttributes]; + NSMutableAttributedString *newString = [nsAttributedString mutableCopy]; + [usedUtils applyMarkdownFormatting:newString withDefaultTextAttributes:defaultNSTextAttributes]; // create a clone of the old TextInputState and update the // attributed string box to point to the string with markdown @@ -246,8 +246,8 @@ stateData.attributedStringBox); // apply markdown - auto newString = [usedUtils parseMarkdown:nsAttributedString - withDefaultTextAttributes:defaultNSTextAttributes]; + NSMutableAttributedString *newString = [nsAttributedString mutableCopy]; + [usedUtils applyMarkdownFormatting:newString withDefaultTextAttributes:defaultNSTextAttributes]; // create a clone of the old TextInputState and update the // attributed string box to point to the string with markdown diff --git a/apple/MarkdownFormatter.h b/apple/MarkdownFormatter.h index 1cf86c851..a219623f6 100644 --- a/apple/MarkdownFormatter.h +++ b/apple/MarkdownFormatter.h @@ -8,10 +8,10 @@ const NSAttributedStringKey RCTLiveMarkdownBlockquoteDepthAttributeName = @"RCTL @interface MarkdownFormatter : NSObject -- (nonnull NSAttributedString *)format:(nonnull NSString *)text - withDefaultTextAttributes:(nonnull NSDictionary *)defaultTextAttributes - withMarkdownRanges:(nonnull NSArray *)markdownRanges - withMarkdownStyle:(nonnull RCTMarkdownStyle *)markdownStyle; +- (void)formatAttributedString:(nonnull NSMutableAttributedString *)attributedString + withDefaultTextAttributes:(nonnull NSDictionary *)defaultTextAttributes + withMarkdownRanges:(nonnull NSArray *)markdownRanges + withMarkdownStyle:(nonnull RCTMarkdownStyle *)markdownStyle; NS_ASSUME_NONNULL_END diff --git a/apple/MarkdownFormatter.mm b/apple/MarkdownFormatter.mm index fdb4116f6..826a55be3 100644 --- a/apple/MarkdownFormatter.mm +++ b/apple/MarkdownFormatter.mm @@ -3,21 +3,22 @@ @implementation MarkdownFormatter -- (nonnull NSAttributedString *)format:(nonnull NSString *)text - withDefaultTextAttributes:(nonnull NSDictionary *)defaultTextAttributes - withMarkdownRanges:(nonnull NSArray *)markdownRanges - withMarkdownStyle:(nonnull RCTMarkdownStyle *)markdownStyle +- (void)formatAttributedString:(nonnull NSMutableAttributedString *)attributedString + withDefaultTextAttributes:(nonnull NSDictionary *)defaultTextAttributes + withMarkdownRanges:(nonnull NSArray *)markdownRanges + withMarkdownStyle:(nonnull RCTMarkdownStyle *)markdownStyle { - NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:text attributes:defaultTextAttributes]; + NSRange fullRange = NSMakeRange(0, attributedString.length); [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)]; + // We cannot simply call `[attributedString setAttributes:@{} range:fullRange];` + // because it makes spellcheck disappear immediately and also makes cursor lag behind while typing fast. + + [attributedString removeAttribute:NSParagraphStyleAttributeName range:fullRange]; + [attributedString removeAttribute:RCTLiveMarkdownBlockquoteDepthAttributeName range:fullRange]; + + [attributedString addAttributes:defaultTextAttributes range:fullRange]; for (MarkdownRange *markdownRange in markdownRanges) { [self applyRangeToAttributedString:attributedString @@ -34,9 +35,25 @@ - (nonnull NSAttributedString *)format:(nonnull NSString *)text RCTApplyBaselineOffset(attributedString, enclosingRange); }]; - [attributedString endEditing]; + /* + Calling `[attributedString addAttributes:defaultTextAttributes range:fullRange]` breaks the font for emojis. + Before, NSFont attribute is ".SFUI-Regular" and NSOriginalFont attribute is ".AppleColorEmoji". + After the call, both are set to ".SFUI-Regular" which makes emoji invisible and zero-width. + Calling `fixAttributesInRange:` fixes this problem. + */ + [attributedString fixAttributesInRange:fullRange]; + + /* + When updating MarkdownTextInput's `style` property without changing `markdownStyle`, + React Native calls `[RCTTextInputComponentView _setAttributedString:]` which skips update if strings are equal. + See https://github.com/facebook/react-native/blob/287e20033207df5e59d199a347b7ae2b4cd7a59e/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm#L680-L684 + The attributed strings are compared using `[RCTTextInputComponentView _textOf:equals:]` which compares only raw strings + if NSOriginalFont attribute is present. So we purposefully remove this attribute to force update. + See https://github.com/facebook/react-native/blob/287e20033207df5e59d199a347b7ae2b4cd7a59e/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm#L751-L784 + */ + [attributedString removeAttribute:@"NSOriginalFont" range:fullRange]; - return attributedString; + [attributedString endEditing]; } - (void)applyRangeToAttributedString:(NSMutableAttributedString *)attributedString diff --git a/apple/MarkdownParser.mm b/apple/MarkdownParser.mm index 4e96050a1..0dc084731 100644 --- a/apple/MarkdownParser.mm +++ b/apple/MarkdownParser.mm @@ -32,7 +32,7 @@ @implementation MarkdownParser { output = markdownRuntime->runGuarded(markdownWorklet, input); } catch (const jsi::JSError &error) { // Skip formatting, runGuarded will show the error in LogBox - _prevText = text; + _prevText = [NSString stringWithString:text]; _prevParserId = parserId; _prevMarkdownRanges = @[]; return _prevMarkdownRanges; @@ -58,13 +58,13 @@ @implementation MarkdownParser { } } catch (const jsi::JSError &error) { RCTLogWarn(@"[react-native-live-markdown] Incorrect schema of worklet parser output: %s", error.getMessage().c_str()); - _prevText = text; + _prevText = [NSString stringWithString:text]; _prevParserId = parserId; _prevMarkdownRanges = @[]; return _prevMarkdownRanges; } - _prevText = text; + _prevText = [NSString stringWithString:text]; _prevParserId = parserId; _prevMarkdownRanges = markdownRanges; return _prevMarkdownRanges; diff --git a/apple/MarkdownTextFieldObserver.h b/apple/MarkdownTextFieldObserver.h new file mode 100644 index 000000000..da6004517 --- /dev/null +++ b/apple/MarkdownTextFieldObserver.h @@ -0,0 +1,17 @@ +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface MarkdownTextFieldObserver : NSObject + +- (instancetype)initWithTextField:(nonnull RCTUITextField *)textField markdownUtils:(nonnull RCTMarkdownUtils *)markdownUtils; + +- (void)textFieldDidChange:(UITextField *)textField; + +- (void)textFieldDidEndEditing:(UITextField *)textField; + +@end + +NS_ASSUME_NONNULL_END diff --git a/apple/MarkdownTextFieldObserver.mm b/apple/MarkdownTextFieldObserver.mm new file mode 100644 index 000000000..d0f94bd8b --- /dev/null +++ b/apple/MarkdownTextFieldObserver.mm @@ -0,0 +1,73 @@ +#import +#import "react_native_assert.h" + +@implementation MarkdownTextFieldObserver { + RCTUITextField *_textField; + RCTMarkdownUtils *_markdownUtils; + BOOL _active; +} + +- (instancetype)initWithTextField:(nonnull RCTUITextField *)textField markdownUtils:(nonnull RCTMarkdownUtils *)markdownUtils +{ + if ((self = [super init])) { + react_native_assert(textField != nil); + react_native_assert(markdownUtils != nil); + + _textField = textField; + _markdownUtils = markdownUtils; + _active = YES; + } + return self; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + if (_active && ([keyPath isEqualToString:@"text"] || [keyPath isEqualToString:@"attributedText"])) { + [self applyMarkdownFormatting]; + } +} + +- (void)textFieldDidChange:(__unused UITextField *)textField +{ + [self applyMarkdownFormatting]; +} + +- (void)textFieldDidEndEditing:(__unused UITextField *)textField +{ + // In order to prevent iOS from applying underline to the whole text if text ends with a link on blur, + // we need to update `defaultTextAttributes` which at this point doesn't contain NSUnderline attribute yet. + // It seems like the setter performs deep comparision, so we differentiate the new value using a counter, + // otherwise this trick would work only once. + static NSAttributedStringKey RCTLiveMarkdownForceUpdateAttributeName = @"RCTLiveMarkdownForceUpdate"; + static NSUInteger counter = 0; + NSMutableDictionary *defaultTextAttributes = [_textField.defaultTextAttributes mutableCopy]; + defaultTextAttributes[RCTLiveMarkdownForceUpdateAttributeName] = @(counter++); + _textField.defaultTextAttributes = defaultTextAttributes; + [self applyMarkdownFormatting]; +} + +- (void)applyMarkdownFormatting +{ + react_native_assert(_textField.defaultTextAttributes != nil); + + if (_textField.markedTextRange != nil) { + return; // skip formatting during multi-stage input to avoid breaking internal state + } + + NSMutableAttributedString *attributedText = [_textField.attributedText mutableCopy]; + [_markdownUtils applyMarkdownFormatting:attributedText withDefaultTextAttributes:_textField.defaultTextAttributes]; + + UITextRange *textRange = _textField.selectedTextRange; + + _active = NO; // prevent recursion + _textField.attributedText = attributedText; + _active = YES; + + // Restore cursor position + [_textField setSelectedTextRange:textRange notifyDelegate:NO]; + + // Eliminate underline blinks while typing if previous text ends with a link + _textField.typingAttributes = _textField.defaultTextAttributes; +} + +@end diff --git a/apple/MarkdownTextInputDecoratorView.mm b/apple/MarkdownTextInputDecoratorView.mm index d0f94ffe6..f3c5e860b 100644 --- a/apple/MarkdownTextInputDecoratorView.mm +++ b/apple/MarkdownTextInputDecoratorView.mm @@ -1,16 +1,18 @@ #import +#import #import "react_native_assert.h" -#import -#import -#import -#import - #ifdef RCT_NEW_ARCH_ENABLED -#import +#import #else -#import -#endif /* RCT_NEW_ARCH_ENABLED */ +#import +#endif + +#import +#import +#import +#import +#import #import @@ -18,14 +20,11 @@ @implementation MarkdownTextInputDecoratorView { RCTMarkdownUtils *_markdownUtils; RCTMarkdownStyle *_markdownStyle; NSNumber *_parserId; -#ifdef RCT_NEW_ARCH_ENABLED - __weak RCTTextInputComponentView *_textInput; -#else - __weak RCTBaseTextInputView *_textInput; -#endif /* RCT_NEW_ARCH_ENABLED */ - __weak UIView *_backedTextInputView; - __weak RCTBackedTextFieldDelegateAdapter *_adapter; + MarkdownBackedTextInputDelegate *_markdownBackedTextInputDelegate; + MarkdownTextStorageDelegate *_markdownTextStorageDelegate; + MarkdownTextFieldObserver *_markdownTextFieldObserver; __weak RCTUITextView *_textView; + __weak RCTUITextField *_textField; } - (void)didMoveToWindow { @@ -52,12 +51,12 @@ - (void)didMoveToWindow { #ifdef RCT_NEW_ARCH_ENABLED react_native_assert([view isKindOfClass:[RCTTextInputComponentView class]] && "Previous sibling component is not an instance of RCTTextInputComponentView."); - _textInput = (RCTTextInputComponentView *)view; - _backedTextInputView = [_textInput valueForKey:@"_backedTextInputView"]; + RCTTextInputComponentView *textInputComponentView = (RCTTextInputComponentView *)view; + UIView *backedTextInputView = [textInputComponentView valueForKey:@"_backedTextInputView"]; #else react_native_assert([view isKindOfClass:[RCTBaseTextInputView class]] && "Previous sibling component is not an instance of RCTBaseTextInputView."); - _textInput = (RCTBaseTextInputView *)view; - _backedTextInputView = _textInput.backedTextInputView; + RCTBaseTextInputView *baseTextInputView = (RCTBaseTextInputView *)view; + UIView *backedTextInputView = baseTextInputView.backedTextInputView; #endif /* RCT_NEW_ARCH_ENABLED */ _markdownUtils = [[RCTMarkdownUtils alloc] init]; @@ -65,14 +64,43 @@ - (void)didMoveToWindow { [_markdownUtils setMarkdownStyle:_markdownStyle]; [_markdownUtils setParserId:_parserId]; - [_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]]) { - _textView = (RCTUITextView *)_backedTextInputView; - [_textView setMarkdownUtils:_markdownUtils]; + if ([backedTextInputView isKindOfClass:[RCTUITextField class]]) { + _textField = (RCTUITextField *)backedTextInputView; + + // make sure `adjustsFontSizeToFitWidth` is disabled, otherwise formatting will be overwritten + react_native_assert(_textField.adjustsFontSizeToFitWidth == NO); + + _markdownTextFieldObserver = [[MarkdownTextFieldObserver alloc] initWithTextField:_textField markdownUtils:_markdownUtils]; + + // register observers for future edits + [_textField addTarget:_markdownTextFieldObserver action:@selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged]; + [_textField addTarget:_markdownTextFieldObserver action:@selector(textFieldDidEndEditing:) forControlEvents:UIControlEventEditingDidEnd]; + [_textField addObserver:_markdownTextFieldObserver forKeyPath:@"text" options:NSKeyValueObservingOptionNew context:NULL]; + [_textField addObserver:_markdownTextFieldObserver forKeyPath:@"attributedText" options:NSKeyValueObservingOptionNew context:NULL]; + + // format initial value + [_markdownTextFieldObserver textFieldDidChange:_textField]; + + // TODO: register blockquotes layout manager + // https://github.com/Expensify/react-native-live-markdown/issues/87 + } else if ([backedTextInputView isKindOfClass:[RCTUITextView class]]) { + _textView = (RCTUITextView *)backedTextInputView; + + // register delegate for future edits + react_native_assert(_textView.textStorage.delegate == nil); + _markdownTextStorageDelegate = [[MarkdownTextStorageDelegate alloc] initWithTextView:_textView markdownUtils:_markdownUtils]; + _textView.textStorage.delegate = _markdownTextStorageDelegate; + + // register delegate for fixing cursor position + _markdownBackedTextInputDelegate = [[MarkdownBackedTextInputDelegate alloc] initWithTextView:_textView]; + +#ifdef RCT_NEW_ARCH_ENABLED + // format initial value + [_textView.textStorage setAttributedString:_textView.attributedText]; +#else + // `_textView.defaultTextAttributes` is nil here, initial value will be passed to `setAttributedText:` that will be called later +#endif + NSLayoutManager *layoutManager = _textView.layoutManager; // switching to TextKit 1 compatibility mode // Correct content height in TextKit 1 compatibility mode. (See https://github.com/Expensify/App/issues/41567) @@ -92,18 +120,24 @@ - (void)didMoveToWindow { - (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]]) { [_textView.layoutManager setValue:nil forKey:@"markdownUtils"]; object_setClass(_textView.layoutManager, [NSLayoutManager class]); } + _markdownBackedTextInputDelegate = nil; + _markdownTextStorageDelegate = nil; + _textView.textStorage.delegate = nil; + _textView = nil; + } + + if (_textField != nil) { + [_textField removeTarget:_markdownTextFieldObserver action:@selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged]; + [_textField removeTarget:_markdownTextFieldObserver action:@selector(textFieldDidEndEditing:) forControlEvents:UIControlEventEditingDidEnd]; + [_textField removeObserver:_markdownTextFieldObserver forKeyPath:@"text" context:NULL]; + [_textField removeObserver:_markdownTextFieldObserver forKeyPath:@"attributedText" context:NULL]; + _markdownTextFieldObserver = nil; + _textField = nil; } } @@ -124,15 +158,10 @@ - (void)setParserId:(NSNumber *)parserId - (void)applyNewStyles { if (_textView != nil) { - // We want to use `textStorage` for applying markdown when possible. Currently it's only available for UITextView - [_textView textDidChange]; - } else { - // apply new styles -#ifdef RCT_NEW_ARCH_ENABLED - [_textInput _setAttributedString:_backedTextInputView.attributedText]; -#else - [_textInput setAttributedText:_textInput.attributedText]; -#endif /* RCT_NEW_ARCH_ENABLED */ + [_textView.textStorage setAttributedString:_textView.attributedText]; + } + if (_textField != nil) { + [_markdownTextFieldObserver textFieldDidChange:_textField]; } } diff --git a/apple/MarkdownTextStorageDelegate.h b/apple/MarkdownTextStorageDelegate.h new file mode 100644 index 000000000..e7cb244aa --- /dev/null +++ b/apple/MarkdownTextStorageDelegate.h @@ -0,0 +1,13 @@ +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface MarkdownTextStorageDelegate : NSObject + +- (instancetype)initWithTextView:(nonnull RCTUITextView *)textView markdownUtils:(nonnull RCTMarkdownUtils *)markdownUtils; + +@end + +NS_ASSUME_NONNULL_END diff --git a/apple/MarkdownTextStorageDelegate.mm b/apple/MarkdownTextStorageDelegate.mm new file mode 100644 index 000000000..95f53d28a --- /dev/null +++ b/apple/MarkdownTextStorageDelegate.mm @@ -0,0 +1,29 @@ +#import +#import "react_native_assert.h" + +@implementation MarkdownTextStorageDelegate { + RCTUITextView *_textView; + RCTMarkdownUtils *_markdownUtils; +} + +- (instancetype)initWithTextView:(nonnull RCTUITextView *)textView markdownUtils:(nonnull RCTMarkdownUtils *)markdownUtils +{ + if ((self = [super init])) { + react_native_assert(textView != nil); + react_native_assert(markdownUtils != nil); + + _textView = textView; + _markdownUtils = markdownUtils; + } + return self; +} + +- (void)textStorage:(NSTextStorage *)textStorage didProcessEditing:(NSTextStorageEditActions)editedMask range:(NSRange)editedRange changeInLength:(NSInteger)delta { + react_native_assert(_textView.defaultTextAttributes != nil); + + [_markdownUtils applyMarkdownFormatting:textStorage withDefaultTextAttributes:_textView.defaultTextAttributes]; + + // TODO: fix spellcheck not working for any of previous words when component value is controlled and contains bold (probably not here though) +} + +@end diff --git a/apple/RCTBackedTextFieldDelegateAdapter+Markdown.h b/apple/RCTBackedTextFieldDelegateAdapter+Markdown.h deleted file mode 100644 index f8ddc1d2a..000000000 --- a/apple/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/apple/RCTBackedTextFieldDelegateAdapter+Markdown.mm b/apple/RCTBackedTextFieldDelegateAdapter+Markdown.mm deleted file mode 100644 index d4d8f8214..000000000 --- a/apple/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 withDefaultTextAttributes: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/apple/RCTBaseTextInputView+Markdown.h b/apple/RCTBaseTextInputView+Markdown.h deleted file mode 100644 index 3d37adb25..000000000 --- a/apple/RCTBaseTextInputView+Markdown.h +++ /dev/null @@ -1,18 +0,0 @@ -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface RCTBaseTextInputView (Markdown) - -@property(nonatomic, nullable, getter=getMarkdownUtils) RCTMarkdownUtils *markdownUtils; - -- (void)markdown_setAttributedText:(NSAttributedString *)attributedText; - -- (BOOL)markdown_textOf:(NSAttributedString *)newText equals:(NSAttributedString *)oldText; - -- (void)markdown_updateLocalData; - -@end - -NS_ASSUME_NONNULL_END diff --git a/apple/RCTBaseTextInputView+Markdown.mm b/apple/RCTBaseTextInputView+Markdown.mm deleted file mode 100644 index ec4200682..000000000 --- a/apple/RCTBaseTextInputView+Markdown.mm +++ /dev/null @@ -1,101 +0,0 @@ -#import -#import -#import - -@implementation RCTBaseTextInputView (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_setAttributedText:(NSAttributedString *)attributedText -{ - RCTMarkdownUtils *markdownUtils = [self getMarkdownUtils]; - if (markdownUtils != nil) { - attributedText = [markdownUtils parseMarkdown:attributedText withDefaultTextAttributes:self.backedTextInputView.defaultTextAttributes]; - } - - // Call the original method - [self markdown_setAttributedText:attributedText]; -} - -- (BOOL)markdown_textOf:(NSAttributedString *)newText equals:(NSAttributedString *)oldText -{ - RCTMarkdownUtils *markdownUtils = [self getMarkdownUtils]; - if (markdownUtils != nil) { - // Emoji characters are automatically assigned an AppleColorEmoji NSFont and the original font is moved to NSOriginalFont - // We need to remove these attributes before comparison - NSMutableAttributedString *newTextCopy = [newText mutableCopy]; - NSMutableAttributedString *oldTextCopy = [oldText mutableCopy]; - [newTextCopy removeAttribute:@"NSFont" range:NSMakeRange(0, newTextCopy.length)]; - [oldTextCopy removeAttribute:@"NSFont" range:NSMakeRange(0, oldTextCopy.length)]; - [oldTextCopy removeAttribute:@"NSOriginalFont" range:NSMakeRange(0, oldTextCopy.length)]; - return [newTextCopy isEqualToAttributedString:oldTextCopy]; - } - - 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 withDefaultTextAttributes: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; - dispatch_once(&onceToken, ^{ - Class cls = [self class]; - - { - // swizzle setAttributedText - SEL originalSelector = @selector(setAttributedText:); - SEL swizzledSelector = @selector(markdown_setAttributedText:); - Method originalMethod = class_getInstanceMethod(cls, originalSelector); - Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector); - 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:); - SEL swizzledSelector = @selector(markdown_textOf:equals:); - Method originalMethod = class_getInstanceMethod(cls, originalSelector); - Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector); - method_exchangeImplementations(originalMethod, swizzledMethod); - } - }); -} - -@end diff --git a/apple/RCTMarkdownUtils.h b/apple/RCTMarkdownUtils.h index fed14596d..12fb1ba9f 100644 --- a/apple/RCTMarkdownUtils.h +++ b/apple/RCTMarkdownUtils.h @@ -8,8 +8,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) RCTMarkdownStyle *markdownStyle; @property (nonatomic) NSNumber *parserId; -- (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input - withDefaultTextAttributes:(nonnull NSDictionary *)defaultTextAttributes; +- (void)applyMarkdownFormatting:(nonnull NSMutableAttributedString *)attributedString + withDefaultTextAttributes:(nonnull NSDictionary *)defaultTextAttributes; @end diff --git a/apple/RCTMarkdownUtils.mm b/apple/RCTMarkdownUtils.mm index a932e572c..d423284a6 100644 --- a/apple/RCTMarkdownUtils.mm +++ b/apple/RCTMarkdownUtils.mm @@ -5,11 +5,6 @@ @implementation RCTMarkdownUtils { MarkdownParser *_markdownParser; MarkdownFormatter *_markdownFormatter; - NSString *_prevInputString; - NSAttributedString *_prevAttributedString; - NSDictionary *_prevDefaultTextAttributes; - __weak RCTMarkdownStyle *_prevMarkdownStyle; - __weak NSNumber *_prevParserId; } - (instancetype)init @@ -22,33 +17,15 @@ - (instancetype)init return self; } -- (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input - withDefaultTextAttributes:(nonnull NSDictionary *)defaultTextAttributes +- (void)applyMarkdownFormatting:(nonnull NSMutableAttributedString *)attributedString + withDefaultTextAttributes:(nonnull NSDictionary *)defaultTextAttributes { - @synchronized (self) { - if (input == nil) { - return nil; - } + NSArray *markdownRanges = [_markdownParser parse:attributedString.string withParserId:_parserId]; - NSString *inputString = [input string]; - if ([inputString isEqualToString:_prevInputString] && [defaultTextAttributes isEqualToDictionary:_prevDefaultTextAttributes] && [_markdownStyle isEqual:_prevMarkdownStyle] && [_parserId isEqualToNumber:_prevParserId]) { - return _prevAttributedString; - } - - NSArray *markdownRanges = [_markdownParser parse:inputString withParserId:_parserId]; - - NSAttributedString *attributedString = [_markdownFormatter format:inputString - withDefaultTextAttributes:defaultTextAttributes - withMarkdownRanges:markdownRanges - withMarkdownStyle:_markdownStyle]; - _prevInputString = inputString; - _prevAttributedString = attributedString; - _prevDefaultTextAttributes = defaultTextAttributes; - _prevMarkdownStyle = _markdownStyle; - _prevParserId = _parserId; - - return attributedString; - } + [_markdownFormatter formatAttributedString:attributedString + withDefaultTextAttributes:defaultTextAttributes + withMarkdownRanges:markdownRanges + withMarkdownStyle:_markdownStyle]; } @end diff --git a/apple/RCTTextInputComponentView+Markdown.h b/apple/RCTTextInputComponentView+Markdown.h deleted file mode 100644 index 346bc7115..000000000 --- a/apple/RCTTextInputComponentView+Markdown.h +++ /dev/null @@ -1,23 +0,0 @@ -// This guard prevent this file to be compiled in the old architecture. -#ifdef RCT_NEW_ARCH_ENABLED - -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface RCTTextInputComponentView (Markdown) - -@property(nonatomic, nullable, getter=getMarkdownUtils) RCTMarkdownUtils *markdownUtils; - -- (void)markdown__setAttributedString:(NSAttributedString *)attributedString; - -- (BOOL)markdown__textOf:(NSAttributedString *)newText equals:(NSAttributedString *)oldText; - -- (void)_setAttributedString:(NSAttributedString *)attributedString; - -@end - -NS_ASSUME_NONNULL_END - -#endif /* RCT_NEW_ARCH_ENABLED */ diff --git a/apple/RCTTextInputComponentView+Markdown.mm b/apple/RCTTextInputComponentView+Markdown.mm deleted file mode 100644 index 6570097c1..000000000 --- a/apple/RCTTextInputComponentView+Markdown.mm +++ /dev/null @@ -1,97 +0,0 @@ -// This guard prevent this file to be compiled in the old architecture. -#ifdef RCT_NEW_ARCH_ENABLED - -#import -#import -#import -#import - -#import "MarkdownShadowFamilyRegistry.h" - -using namespace expensify::livemarkdown; - -@implementation RCTTextInputComponentView (Markdown) - -- (void)setMarkdownUtils:(RCTMarkdownUtils *)markdownUtils { - objc_setAssociatedObject(self, @selector(getMarkdownUtils), markdownUtils, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - - if (markdownUtils != nil) { - // force Markdown formatting on first render because `_setAttributedText` is called before `setMarkdownUtils` - RCTUITextField *backedTextInputView = [self getBackedTextInputView]; - backedTextInputView.attributedText = [markdownUtils parseMarkdown:backedTextInputView.attributedText withDefaultTextAttributes:backedTextInputView.defaultTextAttributes]; - } -} - -- (RCTMarkdownUtils *)getMarkdownUtils { - return objc_getAssociatedObject(self, @selector(getMarkdownUtils)); -} - -- (RCTUITextField *)getBackedTextInputView { - RCTUITextField *backedTextInputView = [self valueForKey:@"_backedTextInputView"]; - return backedTextInputView; -} - -- (void)markdown__setAttributedString:(NSAttributedString *)attributedString -{ - RCTMarkdownUtils *markdownUtils = [self getMarkdownUtils]; - RCTUITextField *backedTextInputView = [self getBackedTextInputView]; - if (markdownUtils != nil && backedTextInputView != nil) { - attributedString = [markdownUtils parseMarkdown:attributedString withDefaultTextAttributes:backedTextInputView.defaultTextAttributes]; - } else { - // If markdownUtils is undefined, the text input hasn't been mounted yet. It will - // update its state with the unformatted attributed string, we want to prevent displaying - // this state by applying markdown in the commit hook where we can read markdown styles - // from decorator props. - MarkdownShadowFamilyRegistry::forceNextStateUpdate((facebook::react::Tag)self.tag); - } - - // Call the original method - [self markdown__setAttributedString:attributedString]; -} - -- (BOOL)markdown__textOf:(NSAttributedString *)newText equals:(NSAttributedString *)oldText -{ - RCTMarkdownUtils *markdownUtils = [self getMarkdownUtils]; - if (markdownUtils != nil) { - // Emoji characters are automatically assigned an AppleColorEmoji NSFont and the original font is moved to NSOriginalFont - // We need to remove these attributes before comparison - NSMutableAttributedString *newTextCopy = [newText mutableCopy]; - NSMutableAttributedString *oldTextCopy = [oldText mutableCopy]; - [newTextCopy removeAttribute:@"NSFont" range:NSMakeRange(0, newTextCopy.length)]; - [oldTextCopy removeAttribute:@"NSFont" range:NSMakeRange(0, oldTextCopy.length)]; - [oldTextCopy removeAttribute:@"NSOriginalFont" range:NSMakeRange(0, oldTextCopy.length)]; - return [newTextCopy isEqualToAttributedString:oldTextCopy]; - } - - return [self markdown__textOf:newText equals:oldText]; -} - -+ (void)load -{ - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - { - // swizzle _setAttributedString - Class cls = [self class]; - SEL originalSelector = @selector(_setAttributedString:); - SEL swizzledSelector = @selector(markdown__setAttributedString:); - Method originalMethod = class_getInstanceMethod(cls, originalSelector); - Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector); - method_exchangeImplementations(originalMethod, swizzledMethod); - } - - { - // swizzle _textOf - Class cls = [self class]; - SEL originalSelector = @selector(_textOf:equals:); - SEL swizzledSelector = @selector(markdown__textOf:equals:); - Method originalMethod = class_getInstanceMethod(cls, originalSelector); - Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector); - method_exchangeImplementations(originalMethod, swizzledMethod); - } - }); -} - -@end - -#endif /* RCT_NEW_ARCH_ENABLED */ diff --git a/apple/RCTUITextView+Markdown.h b/apple/RCTUITextView+Markdown.h deleted file mode 100644 index 40deedad5..000000000 --- a/apple/RCTUITextView+Markdown.h +++ /dev/null @@ -1,19 +0,0 @@ -#import -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface RCTUITextView (Private) -- (void)textDidChange; -@end - -@interface RCTUITextView (Markdown) - -@property(nonatomic, nullable, getter=getMarkdownUtils) RCTMarkdownUtils *markdownUtils; - -- (void)markdown_textDidChange; - -@end - -NS_ASSUME_NONNULL_END diff --git a/apple/RCTUITextView+Markdown.mm b/apple/RCTUITextView+Markdown.mm deleted file mode 100644 index 5a49abe95..000000000 --- a/apple/RCTUITextView+Markdown.mm +++ /dev/null @@ -1,42 +0,0 @@ -#import -#import -#import - -@implementation RCTUITextView (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_textDidChange -{ - RCTMarkdownUtils *markdownUtils = [self getMarkdownUtils]; - if (markdownUtils != nil) { - UITextRange *range = self.selectedTextRange; - super.attributedText = [markdownUtils parseMarkdown:self.attributedText withDefaultTextAttributes: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 - } - - // Call the original method - [self markdown_textDidChange]; -} - -+ (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); - }); -} - -@end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index a8d14f2a2..f8c3e032c 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1497,7 +1497,7 @@ PODS: - React-logger (= 0.75.3) - React-perflogger (= 0.75.3) - React-utils (= 0.75.3) - - RNLiveMarkdown (0.1.210): + - RNLiveMarkdown (0.1.211): - DoubleConversion - glog - hermes-engine @@ -1517,10 +1517,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/newarch (= 0.1.210) + - RNLiveMarkdown/newarch (= 0.1.211) - RNReanimated/worklets - Yoga - - RNLiveMarkdown/newarch (0.1.210): + - RNLiveMarkdown/newarch (0.1.211): - DoubleConversion - glog - hermes-engine @@ -1897,7 +1897,7 @@ SPEC CHECKSUMS: React-utils: f2afa6acd905ca2ce7bb8ffb4a22f7f8a12534e8 ReactCodegen: e35c23cdd36922f6d2990c6c1f1b022ade7ad74d ReactCommon: 289214026502e6a93484f4a46bcc0efa4f3f2864 - RNLiveMarkdown: 687bc45ffb3b4af261f414fea169f10eae5ac261 + RNLiveMarkdown: d14eeb66f85495e6d42829438cdd9cb72d253feb RNReanimated: 75df06d3a81fc147b83056ae469512f573365b1d SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d Yoga: 1354c027ab07c7736f99a3bef16172d6f1b12b47 diff --git a/example/src/App.tsx b/example/src/App.tsx index 77d5e421d..7f49031e9 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -17,6 +17,7 @@ console.log(Animated); export default function App() { const [value, setValue] = React.useState(TEST_CONST.EXAMPLE_CONTENT); + const [multiline, setMultiline] = React.useState(true); const [textColorState, setTextColorState] = React.useState(false); const [linkColorState, setLinkColorState] = React.useState(false); const [textFontSizeState, setTextFontSizeState] = React.useState(false); @@ -47,7 +48,7 @@ export default function App() { +