Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add MarkdownText component for preview #125

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import * as React from 'react';

import { Button, Platform, StyleSheet, Text, View } from 'react-native';
import {
MarkdownText,
MarkdownTextInput,
} from '@expensify/react-native-live-markdown';

import { MarkdownTextInput } from '@expensify/react-native-live-markdown';
import type { TextInput } from 'react-native';

const DEFAULT_TEXT = [
Expand Down Expand Up @@ -112,7 +115,7 @@ export default function App() {
onChangeText={setValue}
style={styles.input}
/> */}
<Text style={styles.text}>{JSON.stringify(value)}</Text>
<MarkdownText style={styles.text}>{value}</MarkdownText>
<Button title="Focus" onPress={() => ref.current?.focus()} />
<Button title="Blur" onPress={() => ref.current?.blur()} />
<Button title="Reset" onPress={() => setValue(DEFAULT_TEXT)} />
Expand Down Expand Up @@ -140,8 +143,7 @@ const styles = StyleSheet.create({
textAlignVertical: 'top',
},
text: {
fontFamily: 'Courier New',
marginTop: 10,
height: 100,
width: 300,
},
});
12 changes: 12 additions & 0 deletions ios/MarkdownTextDecoratorComponentView.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// This guard prevent this file to be compiled in the old architecture.
#ifdef RCT_NEW_ARCH_ENABLED
#import <React/RCTViewComponentView.h>

NS_ASSUME_NONNULL_BEGIN

@interface MarkdownTextDecoratorComponentView : RCTViewComponentView
@end

NS_ASSUME_NONNULL_END

#endif /* RCT_NEW_ARCH_ENABLED */
55 changes: 55 additions & 0 deletions ios/MarkdownTextDecoratorComponentView.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// This guard prevent this file to be compiled in the old architecture.
#ifdef RCT_NEW_ARCH_ENABLED
#import <react/renderer/components/RNLiveMarkdownSpec/ComponentDescriptors.h>
#import <react/renderer/components/RNLiveMarkdownSpec/Props.h>

#import <react-native-live-markdown/MarkdownTextDecoratorComponentView.h>
#import <react-native-live-markdown/MarkdownTextDecoratorView.h>
#import <react-native-live-markdown/RCTMarkdownStyle.h>

#import "RCTFabricComponentsPlugins.h"

using namespace facebook::react;

@implementation MarkdownTextDecoratorComponentView {
MarkdownTextDecoratorView *_view;
}

+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<MarkdownTextDecoratorViewComponentDescriptor>();
}

- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared<const MarkdownTextDecoratorViewProps>();
_props = defaultProps;

_view = [[MarkdownTextDecoratorView alloc] init];

self.contentView = _view;
}

return self;
}

- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
{
const auto &oldViewProps = *std::static_pointer_cast<MarkdownTextDecoratorViewProps const>(_props);
const auto &newViewProps = *std::static_pointer_cast<MarkdownTextDecoratorViewProps const>(props);

// TODO: if (oldViewProps.markdownStyle != newViewProps.markdownStyle)
RCTMarkdownStyle *markdownStyle = [[RCTMarkdownStyle alloc] initWithStruct:newViewProps.markdownStyle];
[_view setMarkdownStyle:markdownStyle];

[super updateProps:props oldProps:oldProps];
}

Class<RCTComponentViewProtocol> MarkdownTextDecoratorViewCls(void)
{
return MarkdownTextDecoratorComponentView.class;
}

@end
#endif
8 changes: 8 additions & 0 deletions ios/MarkdownTextDecoratorView.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#import <UIKit/UIKit.h>
#import <react-native-live-markdown/RCTMarkdownStyle.h>

@interface MarkdownTextDecoratorView : UIView

- (void)setMarkdownStyle:(nonnull RCTMarkdownStyle *)markdownStyle;

@end
93 changes: 93 additions & 0 deletions ios/MarkdownTextDecoratorView.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#import <React/RCTTextView.h>
#import <react/debug/react_native_assert.h>

//#import <react-native-live-markdown/MarkdownLayoutManager.h>
#import <react-native-live-markdown/MarkdownTextDecoratorView.h>
#import <react-native-live-markdown/RCTMarkdownUtils.h>
#import <react-native-live-markdown/RCTMarkdownStyle.h>
//#import <react-native-live-markdown/RCTBackedTextFieldDelegateAdapter+Markdown.h>
#import <react-native-live-markdown/RCTTextView+Markdown.h>

//#ifdef RCT_NEW_ARCH_ENABLED
//#import <react-native-live-markdown/RCTTextComponentView+Markdown.h>
//#else
//#import <react-native-live-markdown/RCTBaseTextView+Markdown.h>
//#endif /* RCT_NEW_ARCH_ENABLED */

#import <objc/runtime.h>

@implementation MarkdownTextDecoratorView {
RCTMarkdownUtils *_markdownUtils;
RCTMarkdownStyle *_markdownStyle;
// #ifdef RCT_NEW_ARCH_ENABLED
// __weak RCTTextComponentView *_text;
// #else
__weak RCTTextView *_text;
// #endif /* RCT_NEW_ARCH_ENABLED */
// __weak RCTBackedTextFieldDelegateAdapter *_adapter;
// __weak RCTUITextView *_textView;
}

- (void)didMoveToWindow {
#ifdef RCT_NEW_ARCH_ENABLED
if (self.superview.superview == nil) {
return;
}
#else
if (self.superview == nil) {
return;
}
#endif /* RCT_NEW_ARCH_ENABLED */

#ifdef RCT_NEW_ARCH_ENABLED
NSArray *viewsArray = self.superview.superview.subviews;
NSUInteger currentIndex = [viewsArray indexOfObject:self.superview];
#else
NSArray *viewsArray = self.superview.subviews;
NSUInteger currentIndex = [viewsArray indexOfObject:self];
#endif /* RCT_NEW_ARCH_ENABLED */

react_native_assert(currentIndex != 0 && currentIndex != NSNotFound && "Error while finding current component.");
UIView *view = [viewsArray objectAtIndex:currentIndex - 1];

#ifdef RCT_NEW_ARCH_ENABLED
// react_native_assert([view isKindOfClass:[RCTTextComponentView class]] && "Previous sibling component is not an instance of RCTTextComponentView.");
// _text = (RCTTextComponentView *)view;
// UIView<RCTBackedTextViewProtocol> *backedTextView = [_text valueForKey:@"_backedTextView"];
#else
react_native_assert([view isKindOfClass:[RCTTextView class]] && "Previous sibling component is not an instance of RCTTextView.");
_text = (RCTTextView *)view;
#endif /* RCT_NEW_ARCH_ENABLED */

_markdownUtils = [[RCTMarkdownUtils alloc] initWithTextView:_text];
react_native_assert(_markdownStyle != nil);
[_markdownUtils setMarkdownStyle:_markdownStyle];

[_text setMarkdownUtils:_markdownUtils];
}

- (void)willMoveToWindow:(UIWindow *)newWindow
{
// if (_text != nil) {
// [_text 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]);
// }
// }
}

- (void)setMarkdownStyle:(nonnull RCTMarkdownStyle *)markdownStyle
{
_markdownStyle = markdownStyle;
[_markdownUtils setMarkdownStyle:markdownStyle];
// [_text textDidChange]; // trigger attributed text update
}

@end
5 changes: 5 additions & 0 deletions ios/MarkdownTextDecoratorViewManager.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#import <React/RCTViewManager.h>

@interface MarkdownTextDecoratorViewManager : RCTViewManager

@end
23 changes: 23 additions & 0 deletions ios/MarkdownTextDecoratorViewManager.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#import <react-native-live-markdown/MarkdownTextDecoratorViewManager.h>
#import <react-native-live-markdown/MarkdownTextDecoratorView.h>

@implementation MarkdownTextDecoratorViewManager

RCT_EXPORT_MODULE(MarkdownTextDecoratorView)

- (UIView *)view
{
return [[MarkdownTextDecoratorView alloc] init];
}

RCT_CUSTOM_VIEW_PROPERTY(markdownStyle, NSDictionary, MarkdownTextDecoratorView)
{
#ifdef RCT_NEW_ARCH_ENABLED
// implemented in MarkdownTextDecoratorView updateProps:
#else
RCTMarkdownStyle *markdownStyle = [[RCTMarkdownStyle alloc] initWithDictionary:json];
[view setMarkdownStyle:markdownStyle];
#endif /* RCT_NEW_ARCH_ENABLED */
}

@end
4 changes: 4 additions & 0 deletions ios/RCTMarkdownUtils.h
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
#import <React/RCTBackedTextInputViewProtocol.h>
#import <React/RCTTextView.h>
#import <react-native-live-markdown/RCTMarkdownStyle.h>

@interface RCTMarkdownUtils : NSObject

@property (nonatomic) RCTMarkdownStyle *markdownStyle;
@property (nonatomic) NSMutableArray<NSValue *> *quoteRanges;
@property (weak, nonatomic) UIView<RCTBackedTextInputViewProtocol> *backedTextInputView;
@property (weak, nonatomic) RCTTextView *textView;

- (instancetype)initWithBackedTextInputView:(UIView<RCTBackedTextInputViewProtocol> *)backedTextInputView;

- (instancetype)initWithTextView:(RCTTextView *)textView;

- (NSAttributedString *)parseMarkdown:(NSAttributedString *)input;

@end
20 changes: 18 additions & 2 deletions ios/RCTMarkdownUtils.mm
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ - (instancetype)initWithBackedTextInputView:(UIView<RCTBackedTextInputViewProtoc
return self;
}

- (instancetype)initWithTextView:(RCTTextView *)textView
{
if (self = [super init]) {
_textView = textView;
}
return self;
}

- (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input
{
RCTAssertMainQueue();
Expand Down Expand Up @@ -46,7 +54,13 @@ - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input
JSValue *result = [function callWithArguments:@[inputString]];
NSArray *ranges = [result toArray];

NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:inputString attributes:_backedTextInputView.defaultTextAttributes];
// NSDictionary<NSAttributedStringKey, id> *defaultTextAttributes = _backedTextInputView.defaultTextAttributes;
// if (defaultTextAttributes == nil) {
// defaultTextAttributes = @{};
// }
// assert(defaultTextAttributes != nil);
// NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:inputString attributes:defaultTextAttributes];
NSMutableAttributedString *attributedString = [input mutableCopy];
[attributedString beginEditing];

// If the attributed string ends with underlined text, blurring the single-line input imprints the underline style across the whole string.
Expand Down Expand Up @@ -80,7 +94,9 @@ - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input
size = _markdownStyle.h1FontSize;
}
UIFont *newFont = [UIFont fontWithDescriptor:newFontDescriptor size:size];
[attributedString addAttribute:NSFontAttributeName value:newFont range:range];
if (newFont != nil) {
[attributedString addAttribute:NSFontAttributeName value:newFont range:range];
}
}

if ([type isEqualToString:@"syntax"]) {
Expand Down
17 changes: 17 additions & 0 deletions ios/RCTTextView+Markdown.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#import <UIKit/UIKit.h>
#import <React/RCTTextView.h>
#import <react-native-live-markdown/RCTMarkdownUtils.h>

NS_ASSUME_NONNULL_BEGIN

@interface RCTTextView (Markdown)

@property(nonatomic, nullable, getter=getMarkdownUtils) RCTMarkdownUtils *markdownUtils;

- (void)markdown_setTextStorage:(NSTextStorage *)textStorage
contentFrame:(CGRect)contentFrame
descendantViews:(NSArray<UIView *> *)descendantViews;

@end

NS_ASSUME_NONNULL_END
44 changes: 44 additions & 0 deletions ios/RCTTextView+Markdown.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#import <react-native-live-markdown/RCTTextView+Markdown.h>
#import <react-native-live-markdown/RCTMarkdownUtils.h>
#import <objc/message.h>

@implementation RCTTextView (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_setTextStorage:(NSTextStorage *)textStorage
contentFrame:(CGRect)contentFrame
descendantViews:(NSArray<UIView *> *)descendantViews
{
RCTMarkdownUtils *markdownUtils = [self getMarkdownUtils];
if (markdownUtils != nil) {
NSRange range = NSMakeRange(0, [textStorage length]);
NSAttributedString *input = [textStorage attributedSubstringFromRange:range];
NSAttributedString *output = [markdownUtils parseMarkdown:input];
[textStorage replaceCharactersInRange:range withAttributedString:output];
}

// Call the original method
[self markdown_setTextStorage:textStorage contentFrame:contentFrame descendantViews:descendantViews];
}

+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = [self class];
SEL originalSelector = @selector(setTextStorage:contentFrame:descendantViews:);
SEL swizzledSelector = @selector(markdown_setTextStorage:contentFrame:descendantViews:);
Method originalMethod = class_getInstanceMethod(cls, originalSelector);
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}

@end
Loading