From f91352f50a839340b6224c3f2fd6e176749e1a8f Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 21 Feb 2024 12:31:42 +0100 Subject: [PATCH 01/35] Update codegen config --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 116b1746..8d3a5f94 100644 --- a/package.json +++ b/package.json @@ -156,7 +156,7 @@ }, "codegenConfig": { "name": "RNLiveMarkdownSpec", - "type": "components", + "type": "all", "jsSrcsDir": "src" } } From 117ddfbb0a3aa8a9092c4998197a607628b890af Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 21 Feb 2024 12:31:59 +0100 Subject: [PATCH 02/35] Update podspec to include cpp files --- RNLiveMarkdown.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RNLiveMarkdown.podspec b/RNLiveMarkdown.podspec index 56ed5a1a..d928b2cd 100644 --- a/RNLiveMarkdown.podspec +++ b/RNLiveMarkdown.podspec @@ -14,7 +14,7 @@ Pod::Spec.new do |s| s.platforms = { :ios => "11.0" } s.source = { :git => "https://github.com/expensify/react-native-live-markdown.git", :tag => "#{s.version}" } - s.source_files = "ios/**/*.{h,m,mm}" + s.source_files = "ios/**/*.{h,m,mm,cpp}" s.resources = "parser/react-native-live-markdown-parser.js" From 5f3d841d97792bd9f40992055ed6b0e979c11024 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 21 Feb 2024 12:32:27 +0100 Subject: [PATCH 03/35] Update js files --- src/MarkdownTextInput.tsx | 5 +++++ src/NativeMarkdownModule.ts | 8 ++++++++ 2 files changed, 13 insertions(+) create mode 100644 src/NativeMarkdownModule.ts diff --git a/src/MarkdownTextInput.tsx b/src/MarkdownTextInput.tsx index 41a00187..bafdea18 100644 --- a/src/MarkdownTextInput.tsx +++ b/src/MarkdownTextInput.tsx @@ -2,10 +2,15 @@ import {StyleSheet, TextInput, processColor} from 'react-native'; import React from 'react'; import type {TextInputProps} from 'react-native'; import MarkdownTextInputDecoratorViewNativeComponent from './MarkdownTextInputDecoratorViewNativeComponent'; +import NativeMarkdownModule from './NativeMarkdownModule'; import type * as MarkdownTextInputDecoratorViewNativeComponentTypes from './MarkdownTextInputDecoratorViewNativeComponent'; import * as StyleUtils from './styleUtils'; import type * as StyleUtilsTypes from './styleUtils'; +if (NativeMarkdownModule) { + NativeMarkdownModule.install(); +} + type PartialMarkdownStyle = StyleUtilsTypes.PartialMarkdownStyle; type MarkdownStyle = MarkdownTextInputDecoratorViewNativeComponentTypes.MarkdownStyle; diff --git a/src/NativeMarkdownModule.ts b/src/NativeMarkdownModule.ts new file mode 100644 index 00000000..4a8328f1 --- /dev/null +++ b/src/NativeMarkdownModule.ts @@ -0,0 +1,8 @@ +import type {TurboModule} from 'react-native'; +import {TurboModuleRegistry} from 'react-native'; + +interface Spec extends TurboModule { + install: () => boolean; +} + +export default TurboModuleRegistry.get('RNLiveMarkdownModule'); From 4153a935e62f552f8f72f62277adc9044c564d5e Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 21 Feb 2024 12:33:01 +0100 Subject: [PATCH 04/35] POC implementation --- ios/MarkdownCommitHook.h | 50 +++++ ios/MarkdownCommitHook.mm | 80 ++++++++ ...ownTextInputDecoratorComponentDescriptor.h | 18 ++ ...MarkdownTextInputDecoratorComponentView.mm | 21 +- ios/MarkdownTextInputDecoratorShadowNode.cpp | 30 +++ ios/MarkdownTextInputDecoratorShadowNode.h | 27 +++ ios/MarkdownTextInputDecoratorState.h | 21 ++ ios/MarkdownTextInputDecoratorView.mm | 3 + ios/RCTMarkdownUtils.h | 1 + ios/RCTMarkdownUtils.mm | 183 ++++++++++-------- ios/RNLiveMarkdownModule.h | 13 ++ ios/RNLiveMarkdownModule.mm | 38 ++++ 12 files changed, 398 insertions(+), 87 deletions(-) create mode 100644 ios/MarkdownCommitHook.h create mode 100644 ios/MarkdownCommitHook.mm create mode 100644 ios/MarkdownTextInputDecoratorComponentDescriptor.h create mode 100644 ios/MarkdownTextInputDecoratorShadowNode.cpp create mode 100644 ios/MarkdownTextInputDecoratorShadowNode.h create mode 100644 ios/MarkdownTextInputDecoratorState.h create mode 100644 ios/RNLiveMarkdownModule.h create mode 100644 ios/RNLiveMarkdownModule.mm diff --git a/ios/MarkdownCommitHook.h b/ios/MarkdownCommitHook.h new file mode 100644 index 00000000..b2f9529d --- /dev/null +++ b/ios/MarkdownCommitHook.h @@ -0,0 +1,50 @@ +#pragma once +#ifdef RCT_NEW_ARCH_ENABLED + +#include +#include +#include + +#include + +#include "MarkdownTextInputDecoratorShadowNode.h" + +using namespace facebook::react; + +namespace livemarkdown { + +struct MarkdownTextInputNode { + std::shared_ptr textInput; + std::shared_ptr decorator; +}; + +class MarkdownCommitHook : public UIManagerCommitHook { + public: + MarkdownCommitHook(const std::shared_ptr &uiManager); + + ~MarkdownCommitHook() noexcept override; + + void commitHookWasRegistered(UIManager const &) const noexcept override {} + + void commitHookWasUnregistered(UIManager const &) const noexcept override {} + + RootShadowNode::Unshared shadowTreeWillCommit( + ShadowTree const &shadowTree, + RootShadowNode::Shared const &oldRootShadowNode, + RootShadowNode::Unshared const &newRootShadowNode) + const noexcept override; + + void setTextInputFamily(ShadowNodeFamily* textInputFamily) { + textInputFamily_ = textInputFamily; + } + + private: + void findTextInputNodes(std::shared_ptr node, std::vector &output) const; + + ShadowNodeFamily* textInputFamily_; + std::shared_ptr uiManager_; +}; + +} // namespace reanimated + +#endif // RCT_NEW_ARCH_ENABLED diff --git a/ios/MarkdownCommitHook.mm b/ios/MarkdownCommitHook.mm new file mode 100644 index 00000000..4602d882 --- /dev/null +++ b/ios/MarkdownCommitHook.mm @@ -0,0 +1,80 @@ + +#ifdef RCT_NEW_ARCH_ENABLED + +#include +#import + +#include "MarkdownCommitHook.h" +#include "RCTMarkdownStyle.h" +#include "RCTMarkdownUtils.h" + +using namespace facebook::react; + +namespace livemarkdown { + +MarkdownCommitHook::MarkdownCommitHook(const std::shared_ptr &uiManager) : uiManager_(uiManager) { + uiManager_->registerCommitHook(*this); +} + +MarkdownCommitHook::~MarkdownCommitHook() noexcept { + uiManager_->unregisterCommitHook(*this); +} + +RootShadowNode::Unshared MarkdownCommitHook::shadowTreeWillCommit( + ShadowTree const &, + RootShadowNode::Shared const &, + RootShadowNode::Unshared const &newRootShadowNode) const noexcept { + auto rootNode = newRootShadowNode->ShadowNode::clone(ShadowNodeFragment{}); + + std::vector nodesToUpdate; + + this->findTextInputNodes(newRootShadowNode, nodesToUpdate); + + for (auto &nodes : nodesToUpdate) { + rootNode = rootNode->cloneTree(nodes.textInput->getFamily(), [&nodes](const ShadowNode& node){ + const auto &textInputState = *std::static_pointer_cast>(nodes.textInput->getState()); + const auto &stateData = textInputState.getData(); + if (stateData.attributedStringBox.getMode() == AttributedStringBox::Mode::Value) { + const auto &markdownProps = *std::static_pointer_cast(nodes.decorator->getProps()); + RCTMarkdownStyle *markdownStyle = [[RCTMarkdownStyle alloc] initWithStruct:markdownProps.markdownStyle]; + RCTMarkdownUtils *utils = [[RCTMarkdownUtils alloc] initWithBackedTextInputView:nil]; + [utils setMarkdownStyle:markdownStyle]; + + auto nsAttributedString = RCTNSAttributedStringFromAttributedStringBox(stateData.attributedStringBox); + auto newString = [utils parseMarkdown:nsAttributedString withAttributes:nil]; + + auto newStateData = std::make_shared(stateData); + newStateData->attributedStringBox = RCTAttributedStringBoxFromNSAttributedString(newString); + + return node.clone({ + .state = std::make_shared>(newStateData, textInputState), + }); + } + + return node.clone({}); + }); + } + + return std::static_pointer_cast(rootNode); +} + +void MarkdownCommitHook::findTextInputNodes(std::shared_ptr node, std::vector &output) const { + std::shared_ptr lastTextInput = nullptr; + + for (auto child : node->getChildren()) { + if (auto textInputNode = std::dynamic_pointer_cast(child)) { + lastTextInput = textInputNode; + } else if (lastTextInput != nullptr) { + if (auto markdownNode = std::dynamic_pointer_cast(child)) { + MarkdownTextInputNode result = {lastTextInput, markdownNode}; + output.push_back(result); + } + } + + findTextInputNodes(child, output); + } +} + +} // namespace livemarkdown + +#endif // RCT_NEW_ARCH_ENABLED diff --git a/ios/MarkdownTextInputDecoratorComponentDescriptor.h b/ios/MarkdownTextInputDecoratorComponentDescriptor.h new file mode 100644 index 00000000..ab2901ea --- /dev/null +++ b/ios/MarkdownTextInputDecoratorComponentDescriptor.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include "MarkdownTextInputDecoratorShadowNode.h" +#include + +namespace facebook { +namespace react { + +class MarkdownTextInputDecoratorComponentDescriptor final + : public ConcreteComponentDescriptor { + public: + using ConcreteComponentDescriptor::ConcreteComponentDescriptor; +}; + +} // namespace react +} // namespace facebook + diff --git a/ios/MarkdownTextInputDecoratorComponentView.mm b/ios/MarkdownTextInputDecoratorComponentView.mm index 91d46656..42c30b44 100644 --- a/ios/MarkdownTextInputDecoratorComponentView.mm +++ b/ios/MarkdownTextInputDecoratorComponentView.mm @@ -1,6 +1,6 @@ // This guard prevent this file to be compiled in the old architecture. #ifdef RCT_NEW_ARCH_ENABLED -#import +#import "MarkdownTextInputDecoratorComponentDescriptor.h" #import #import @@ -13,11 +13,12 @@ @implementation MarkdownTextInputDecoratorComponentView { MarkdownTextInputDecoratorView *_view; + const ShadowNodeFamily *_textInputFamily; } + (ComponentDescriptorProvider)componentDescriptorProvider { - return concreteComponentDescriptorProvider(); + return concreteComponentDescriptorProvider(); } - (instancetype)initWithFrame:(CGRect)frame @@ -34,6 +35,22 @@ - (instancetype)initWithFrame:(CGRect)frame return self; } +- (void)updateState:(const facebook::react::State::Shared &)state oldState:(const facebook::react::State::Shared &)oldState +{ + auto data = std::static_pointer_cast(state)->getData(); + + if (data.textInputFamily != _textInputFamily) { + _textInputFamily = data.textInputFamily; + return; + } +} + +- (void)willMoveToSuperview:(UIView *)newSuperview { + _textInputFamily = nullptr; + + [super willMoveToSuperview:newSuperview]; +} + - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps { const auto &oldViewProps = *std::static_pointer_cast(_props); diff --git a/ios/MarkdownTextInputDecoratorShadowNode.cpp b/ios/MarkdownTextInputDecoratorShadowNode.cpp new file mode 100644 index 00000000..944928aa --- /dev/null +++ b/ios/MarkdownTextInputDecoratorShadowNode.cpp @@ -0,0 +1,30 @@ +#include "MarkdownTextInputDecoratorShadowNode.h" + +#include + +namespace facebook { +namespace react { + +extern const char MarkdownTextInputDecoratorViewComponentName[] = "MarkdownTextInputDecoratorView"; + +void MarkdownTextInputDecoratorShadowNode::layout(LayoutContext layoutContext) { + for (auto it = layoutContext.affectedNodes->rbegin(); it != layoutContext.affectedNodes->rend(); ++it) { + if (const TextInputShadowNode* tiNode = dynamic_cast(*it)) { + auto state = + std::static_pointer_cast( + getState()); + const auto& textInputFamily = tiNode->getFamily(); + + if (state->getData().textInputFamily != &textInputFamily) { + this->dirtyLayout(); + state->updateState(MarkdownTextInputDecoratorState(&textInputFamily)); + } + break; + } + } + + ConcreteShadowNode::layout(layoutContext); +} + +} // namespace react +} // namespace facebook diff --git a/ios/MarkdownTextInputDecoratorShadowNode.h b/ios/MarkdownTextInputDecoratorShadowNode.h new file mode 100644 index 00000000..726cd6da --- /dev/null +++ b/ios/MarkdownTextInputDecoratorShadowNode.h @@ -0,0 +1,27 @@ +#pragma once + +#include "MarkdownTextInputDecoratorState.h" +#include +#include +#include +#include +#include + +namespace facebook { +namespace react { + +JSI_EXPORT extern const char MarkdownTextInputDecoratorViewComponentName[]; + +class JSI_EXPORT MarkdownTextInputDecoratorShadowNode final : public ConcreteViewShadowNode< + MarkdownTextInputDecoratorViewComponentName, + MarkdownTextInputDecoratorViewProps, + MarkdownTextInputDecoratorViewEventEmitter, + MarkdownTextInputDecoratorState> { + public: + using ConcreteViewShadowNode::ConcreteViewShadowNode; + + void layout(LayoutContext layoutContext) override; +}; + +} // namespace react +} // namespace facebook diff --git a/ios/MarkdownTextInputDecoratorState.h b/ios/MarkdownTextInputDecoratorState.h new file mode 100644 index 00000000..4e9aa72c --- /dev/null +++ b/ios/MarkdownTextInputDecoratorState.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +namespace facebook { +namespace react { + +class JSI_EXPORT MarkdownTextInputDecoratorState final { + public: + using Shared = std::shared_ptr; + + MarkdownTextInputDecoratorState() : textInputFamily() {}; + MarkdownTextInputDecoratorState(const ShadowNodeFamily *textInputFamily_) : textInputFamily(textInputFamily_) {}; + + const ShadowNodeFamily* textInputFamily; + +#pragma mark - Getters +}; + +} // namespace react +} // namespace facebook diff --git a/ios/MarkdownTextInputDecoratorView.mm b/ios/MarkdownTextInputDecoratorView.mm index f94f5a9f..c8c1228d 100644 --- a/ios/MarkdownTextInputDecoratorView.mm +++ b/ios/MarkdownTextInputDecoratorView.mm @@ -63,6 +63,9 @@ - (void)didMoveToWindow { [_markdownUtils setMarkdownStyle:_markdownStyle]; [_textInput setMarkdownUtils:_markdownUtils]; + +// [_textInput performSelector:@selector(_updateState)]; + if ([backedTextInputView isKindOfClass:[RCTUITextField class]]) { RCTUITextField *textField = (RCTUITextField *)backedTextInputView; _adapter = [textField valueForKey:@"textInputDelegateAdapter"]; diff --git a/ios/RCTMarkdownUtils.h b/ios/RCTMarkdownUtils.h index a39eb75c..9217748a 100644 --- a/ios/RCTMarkdownUtils.h +++ b/ios/RCTMarkdownUtils.h @@ -12,6 +12,7 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithBackedTextInputView:(UIView *)backedTextInputView; - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input; +- (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withAttributes:(nullable NSDictionary*)attributes; @end diff --git a/ios/RCTMarkdownUtils.mm b/ios/RCTMarkdownUtils.mm index 06600099..1344cd0e 100644 --- a/ios/RCTMarkdownUtils.mm +++ b/ios/RCTMarkdownUtils.mm @@ -32,103 +32,116 @@ - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input 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"]; - } + NSAttributedString *attributedString = [self parseMarkdown:input withAttributes:_backedTextInputView.defaultTextAttributes]; - JSValue *result = [function callWithArguments:@[inputString]]; - NSArray *ranges = [result toArray]; + _prevInputString = inputString; + _prevAttributedString = attributedString; + _prevTextAttributes = _backedTextInputView.defaultTextAttributes; + _prevMarkdownStyle = _markdownStyle; - NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:inputString attributes:_backedTextInputView.defaultTextAttributes]; - [attributedString beginEditing]; + return attributedString; +} - // 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)]; +- (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withAttributes:(nullable NSDictionary*)attributes +{ + if (input == nil) { + return nil; + } - _blockquoteRanges = [NSMutableArray new]; + NSString *inputString = [input string]; + + 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"]; + } - [ranges enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - NSArray *item = obj; - NSString *type = item[0]; - NSUInteger location = [item[1] unsignedIntegerValue]; - NSUInteger length = [item[2] unsignedIntegerValue]; - NSRange range = NSMakeRange(location, length); + JSValue *result = [function callWithArguments:@[inputString]]; + NSArray *ranges = [result toArray]; + + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:inputString attributes:_backedTextInputView.defaultTextAttributes]; + [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)]; + + _blockquoteRanges = [NSMutableArray new]; + + [ranges enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + NSArray *item = obj; + NSString *type = item[0]; + NSUInteger location = [item[1] unsignedIntegerValue]; + NSUInteger length = [item[2] unsignedIntegerValue]; + NSRange range = NSMakeRange(location, length); + + if ([type isEqualToString:@"bold"] || [type isEqualToString:@"italic"] || [type isEqualToString:@"code"] || [type isEqualToString:@"pre"] || [type isEqualToString:@"h1"]) { + 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]; + } else if ([type isEqualToString:@"pre"]) { + font = [RCTFont updateFont:font withFamily:_markdownStyle.preFontFamily]; + } else if ([type isEqualToString:@"h1"]) { + font = [RCTFont updateFont:font withFamily:nil + size:[NSNumber numberWithFloat:_markdownStyle.h1FontSize] + weight:@"bold" + style:nil + variant:nil + scaleMultiplier:0]; + } + [attributedString addAttribute:NSFontAttributeName value:font range:range]; + } - if ([type isEqualToString:@"bold"] || [type isEqualToString:@"italic"] || [type isEqualToString:@"code"] || [type isEqualToString:@"pre"] || [type isEqualToString:@"h1"]) { - 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"]; + 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"]) { - font = [RCTFont updateFont:font withFamily:_markdownStyle.codeFontFamily]; + [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:@"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; + NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; + paragraphStyle.firstLineHeadIndent = indent; + paragraphStyle.headIndent = indent; + [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range]; + [_blockquoteRanges addObject:[NSValue valueWithRange:range]]; } else if ([type isEqualToString:@"pre"]) { - font = [RCTFont updateFont:font withFamily:_markdownStyle.preFontFamily]; + [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"]) { - font = [RCTFont updateFont:font withFamily:nil - size:[NSNumber numberWithFloat:_markdownStyle.h1FontSize] - weight:@"bold" - style:nil - variant:nil - scaleMultiplier:0]; + 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]; } - [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:@"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; - NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; - paragraphStyle.firstLineHeadIndent = indent; - paragraphStyle.headIndent = indent; - [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range]; - [_blockquoteRanges addObject:[NSValue valueWithRange:range]]; - } 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]; - } - }]; - - [attributedString endEditing]; + }]; - _prevInputString = inputString; - _prevAttributedString = attributedString; - _prevTextAttributes = _backedTextInputView.defaultTextAttributes; - _prevMarkdownStyle = _markdownStyle; + [attributedString endEditing]; - return attributedString; + return attributedString; } @end diff --git a/ios/RNLiveMarkdownModule.h b/ios/RNLiveMarkdownModule.h new file mode 100644 index 00000000..205f6182 --- /dev/null +++ b/ios/RNLiveMarkdownModule.h @@ -0,0 +1,13 @@ +#ifdef RCT_NEW_ARCH_ENABLED + +#import +#import +#import + + +@interface RNLiveMarkdownModule : RCTEventEmitter + +@end + +#endif // RCT_NEW_ARCH_ENABLED + diff --git a/ios/RNLiveMarkdownModule.mm b/ios/RNLiveMarkdownModule.mm new file mode 100644 index 00000000..aa5016c2 --- /dev/null +++ b/ios/RNLiveMarkdownModule.mm @@ -0,0 +1,38 @@ +#ifdef RCT_NEW_ARCH_ENABLED + +#import +#import + +#import "RNLiveMarkdownModule.h" +#import "MarkdownCommitHook.h" + +@implementation RNLiveMarkdownModule { + BOOL installed_; + std::shared_ptr commitHook_; +} + +RCT_EXPORT_MODULE(@"RNLiveMarkdownModule") + +- (NSNumber*)install +{ + if (!installed_) { + installed_ = YES; + + RCTBridge *bridge = self.bridge; + RCTSurfacePresenter *surfacePresenter = bridge.surfacePresenter; + RCTScheduler *scheduler = [surfacePresenter scheduler]; + + commitHook_ = std::make_shared(scheduler.uiManager); + } + return @1; +} + +- (std::shared_ptr)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} + +@end + +#endif // RCT_NEW_ARCH_ENABLED + From 23c1f30647e3d96da02af8eaf2953716c7f25404 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 21 Feb 2024 15:49:43 +0100 Subject: [PATCH 05/35] Pass decorator shadow node family in initial state --- ...MarkdownTextInputDecoratorComponentView.mm | 12 +++++------ ios/MarkdownTextInputDecoratorShadowNode.cpp | 18 ---------------- ios/MarkdownTextInputDecoratorShadowNode.h | 21 +++++++++++++++++-- ios/MarkdownTextInputDecoratorState.h | 6 +++--- 4 files changed, 27 insertions(+), 30 deletions(-) diff --git a/ios/MarkdownTextInputDecoratorComponentView.mm b/ios/MarkdownTextInputDecoratorComponentView.mm index 42c30b44..1ea97bcc 100644 --- a/ios/MarkdownTextInputDecoratorComponentView.mm +++ b/ios/MarkdownTextInputDecoratorComponentView.mm @@ -13,7 +13,7 @@ @implementation MarkdownTextInputDecoratorComponentView { MarkdownTextInputDecoratorView *_view; - const ShadowNodeFamily *_textInputFamily; + ShadowNodeFamily::Shared _decoratorFamily; } + (ComponentDescriptorProvider)componentDescriptorProvider @@ -38,15 +38,13 @@ - (instancetype)initWithFrame:(CGRect)frame - (void)updateState:(const facebook::react::State::Shared &)state oldState:(const facebook::react::State::Shared &)oldState { auto data = std::static_pointer_cast(state)->getData(); - - if (data.textInputFamily != _textInputFamily) { - _textInputFamily = data.textInputFamily; - return; - } + _decoratorFamily = data.decoratorFamily; } - (void)willMoveToSuperview:(UIView *)newSuperview { - _textInputFamily = nullptr; + if (newSuperview == nil) { + _decoratorFamily = nullptr; + } [super willMoveToSuperview:newSuperview]; } diff --git a/ios/MarkdownTextInputDecoratorShadowNode.cpp b/ios/MarkdownTextInputDecoratorShadowNode.cpp index 944928aa..10cbb2eb 100644 --- a/ios/MarkdownTextInputDecoratorShadowNode.cpp +++ b/ios/MarkdownTextInputDecoratorShadowNode.cpp @@ -7,24 +7,6 @@ namespace react { extern const char MarkdownTextInputDecoratorViewComponentName[] = "MarkdownTextInputDecoratorView"; -void MarkdownTextInputDecoratorShadowNode::layout(LayoutContext layoutContext) { - for (auto it = layoutContext.affectedNodes->rbegin(); it != layoutContext.affectedNodes->rend(); ++it) { - if (const TextInputShadowNode* tiNode = dynamic_cast(*it)) { - auto state = - std::static_pointer_cast( - getState()); - const auto& textInputFamily = tiNode->getFamily(); - - if (state->getData().textInputFamily != &textInputFamily) { - this->dirtyLayout(); - state->updateState(MarkdownTextInputDecoratorState(&textInputFamily)); - } - break; - } - } - - ConcreteShadowNode::layout(layoutContext); -} } // namespace react } // namespace facebook diff --git a/ios/MarkdownTextInputDecoratorShadowNode.h b/ios/MarkdownTextInputDecoratorShadowNode.h index 726cd6da..d312f082 100644 --- a/ios/MarkdownTextInputDecoratorShadowNode.h +++ b/ios/MarkdownTextInputDecoratorShadowNode.h @@ -12,15 +12,32 @@ namespace react { JSI_EXPORT extern const char MarkdownTextInputDecoratorViewComponentName[]; +static const ShadowNodeFragment::Value createFirstState(ShadowNodeFragment const &fragment, ShadowNodeFamily::Shared const &family) { + const auto newStateData = std::make_shared(family); + + return ShadowNodeFragment::Value({ + .props = fragment.props, + .children = fragment.children, + .state = std::make_shared>(newStateData, *fragment.state), + }); +} + class JSI_EXPORT MarkdownTextInputDecoratorShadowNode final : public ConcreteViewShadowNode< MarkdownTextInputDecoratorViewComponentName, MarkdownTextInputDecoratorViewProps, MarkdownTextInputDecoratorViewEventEmitter, MarkdownTextInputDecoratorState> { public: - using ConcreteViewShadowNode::ConcreteViewShadowNode; + MarkdownTextInputDecoratorShadowNode( + ShadowNodeFragment const &fragment, + ShadowNodeFamily::Shared const &family, + ShadowNodeTraits traits) + : ConcreteViewShadowNode(static_cast(createFirstState(fragment, family)), family, traits) {} - void layout(LayoutContext layoutContext) override; + MarkdownTextInputDecoratorShadowNode( + ShadowNode const &sourceShadowNode, + ShadowNodeFragment const &fragment) + : ConcreteViewShadowNode(sourceShadowNode, fragment) {} }; } // namespace react diff --git a/ios/MarkdownTextInputDecoratorState.h b/ios/MarkdownTextInputDecoratorState.h index 4e9aa72c..144ca580 100644 --- a/ios/MarkdownTextInputDecoratorState.h +++ b/ios/MarkdownTextInputDecoratorState.h @@ -9,10 +9,10 @@ class JSI_EXPORT MarkdownTextInputDecoratorState final { public: using Shared = std::shared_ptr; - MarkdownTextInputDecoratorState() : textInputFamily() {}; - MarkdownTextInputDecoratorState(const ShadowNodeFamily *textInputFamily_) : textInputFamily(textInputFamily_) {}; + MarkdownTextInputDecoratorState() : decoratorFamily(nullptr) {}; + MarkdownTextInputDecoratorState(const ShadowNodeFamily::Shared textInputFamily_) : decoratorFamily(textInputFamily_) {}; - const ShadowNodeFamily* textInputFamily; + const ShadowNodeFamily::Shared decoratorFamily; #pragma mark - Getters }; From 5bcc732d4f4ebd8515648dec1b5231c7c4e76a52 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 21 Feb 2024 15:54:44 +0100 Subject: [PATCH 06/35] Move implementation --- ios/MarkdownTextInputDecoratorShadowNode.cpp | 9 +++++++++ ios/MarkdownTextInputDecoratorShadowNode.h | 14 ++++---------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/ios/MarkdownTextInputDecoratorShadowNode.cpp b/ios/MarkdownTextInputDecoratorShadowNode.cpp index 10cbb2eb..e52ee1db 100644 --- a/ios/MarkdownTextInputDecoratorShadowNode.cpp +++ b/ios/MarkdownTextInputDecoratorShadowNode.cpp @@ -7,6 +7,15 @@ namespace react { extern const char MarkdownTextInputDecoratorViewComponentName[] = "MarkdownTextInputDecoratorView"; +const ShadowNodeFragment::Value MarkdownTextInputDecoratorShadowNode::updateFragmentState(ShadowNodeFragment const &fragment, ShadowNodeFamily::Shared const &family) { + const auto newStateData = std::make_shared(family); + + return ShadowNodeFragment::Value({ + .props = fragment.props, + .children = fragment.children, + .state = std::make_shared(newStateData, *fragment.state), + }); +} } // namespace react } // namespace facebook diff --git a/ios/MarkdownTextInputDecoratorShadowNode.h b/ios/MarkdownTextInputDecoratorShadowNode.h index d312f082..cd3aea68 100644 --- a/ios/MarkdownTextInputDecoratorShadowNode.h +++ b/ios/MarkdownTextInputDecoratorShadowNode.h @@ -12,15 +12,6 @@ namespace react { JSI_EXPORT extern const char MarkdownTextInputDecoratorViewComponentName[]; -static const ShadowNodeFragment::Value createFirstState(ShadowNodeFragment const &fragment, ShadowNodeFamily::Shared const &family) { - const auto newStateData = std::make_shared(family); - - return ShadowNodeFragment::Value({ - .props = fragment.props, - .children = fragment.children, - .state = std::make_shared>(newStateData, *fragment.state), - }); -} class JSI_EXPORT MarkdownTextInputDecoratorShadowNode final : public ConcreteViewShadowNode< MarkdownTextInputDecoratorViewComponentName, @@ -32,12 +23,15 @@ class JSI_EXPORT MarkdownTextInputDecoratorShadowNode final : public ConcreteVie ShadowNodeFragment const &fragment, ShadowNodeFamily::Shared const &family, ShadowNodeTraits traits) - : ConcreteViewShadowNode(static_cast(createFirstState(fragment, family)), family, traits) {} + : ConcreteViewShadowNode(static_cast(updateFragmentState(fragment, family)), family, traits) {} MarkdownTextInputDecoratorShadowNode( ShadowNode const &sourceShadowNode, ShadowNodeFragment const &fragment) : ConcreteViewShadowNode(sourceShadowNode, fragment) {} + + private: + static const ShadowNodeFragment::Value updateFragmentState(ShadowNodeFragment const &fragment, ShadowNodeFamily::Shared const &family); }; } // namespace react From 49817582e5e6756fcd3893c2630e053d9349ba56 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 21 Feb 2024 17:02:58 +0100 Subject: [PATCH 07/35] Don't scan the entire tree when looking for text inputs --- ios/MarkdownCommitHook.h | 7 ---- ios/MarkdownCommitHook.mm | 40 ++++++++++--------- ...MarkdownTextInputDecoratorComponentView.mm | 8 ++++ ios/RNLiveMarkdownModule.h | 4 ++ ios/RNLiveMarkdownModule.mm | 34 ++++++++++++++++ 5 files changed, 67 insertions(+), 26 deletions(-) diff --git a/ios/MarkdownCommitHook.h b/ios/MarkdownCommitHook.h index b2f9529d..818c48c3 100644 --- a/ios/MarkdownCommitHook.h +++ b/ios/MarkdownCommitHook.h @@ -34,14 +34,7 @@ class MarkdownCommitHook : public UIManagerCommitHook { RootShadowNode::Unshared const &newRootShadowNode) const noexcept override; - void setTextInputFamily(ShadowNodeFamily* textInputFamily) { - textInputFamily_ = textInputFamily; - } - private: - void findTextInputNodes(std::shared_ptr node, std::vector &output) const; - - ShadowNodeFamily* textInputFamily_; std::shared_ptr uiManager_; }; diff --git a/ios/MarkdownCommitHook.mm b/ios/MarkdownCommitHook.mm index 4602d882..e2fd869d 100644 --- a/ios/MarkdownCommitHook.mm +++ b/ios/MarkdownCommitHook.mm @@ -7,6 +7,7 @@ #include "MarkdownCommitHook.h" #include "RCTMarkdownStyle.h" #include "RCTMarkdownUtils.h" +#include "RNLiveMarkdownModule.h" using namespace facebook::react; @@ -28,9 +29,27 @@ std::vector nodesToUpdate; - this->findTextInputNodes(newRootShadowNode, nodesToUpdate); + [RNLiveMarkdownModule runForEveryFamily:[&rootNode, &nodesToUpdate](ShadowNodeFamily::Shared family) { + auto ancestors = family->getAncestors(*rootNode); + + if (!ancestors.empty()) { + auto &parentNode = ancestors.back().first.get(); + auto index = ancestors.back().second; + + auto markdownNode = parentNode.getChildren().at(index); + auto previousSibling = parentNode.getChildren().at(index - 1); + + if (auto textInputNode = std::dynamic_pointer_cast(previousSibling)) { + nodesToUpdate.push_back({ + textInputNode, + std::dynamic_pointer_cast(markdownNode), + }); + } + } + }]; for (auto &nodes : nodesToUpdate) { + auto tag = nodes.textInput->getTag(); rootNode = rootNode->cloneTree(nodes.textInput->getFamily(), [&nodes](const ShadowNode& node){ const auto &textInputState = *std::static_pointer_cast>(nodes.textInput->getState()); const auto &stateData = textInputState.getData(); @@ -41,7 +60,7 @@ [utils setMarkdownStyle:markdownStyle]; auto nsAttributedString = RCTNSAttributedStringFromAttributedStringBox(stateData.attributedStringBox); - auto newString = [utils parseMarkdown:nsAttributedString withAttributes:nil]; + auto newString = [utils parseMarkdownWithPreviousAttributes:nsAttributedString]; auto newStateData = std::make_shared(stateData); newStateData->attributedStringBox = RCTAttributedStringBoxFromNSAttributedString(newString); @@ -58,23 +77,6 @@ return std::static_pointer_cast(rootNode); } -void MarkdownCommitHook::findTextInputNodes(std::shared_ptr node, std::vector &output) const { - std::shared_ptr lastTextInput = nullptr; - - for (auto child : node->getChildren()) { - if (auto textInputNode = std::dynamic_pointer_cast(child)) { - lastTextInput = textInputNode; - } else if (lastTextInput != nullptr) { - if (auto markdownNode = std::dynamic_pointer_cast(child)) { - MarkdownTextInputNode result = {lastTextInput, markdownNode}; - output.push_back(result); - } - } - - findTextInputNodes(child, output); - } -} - } // namespace livemarkdown #endif // RCT_NEW_ARCH_ENABLED diff --git a/ios/MarkdownTextInputDecoratorComponentView.mm b/ios/MarkdownTextInputDecoratorComponentView.mm index 1ea97bcc..b39dd4e2 100644 --- a/ios/MarkdownTextInputDecoratorComponentView.mm +++ b/ios/MarkdownTextInputDecoratorComponentView.mm @@ -8,6 +8,7 @@ #import #import "RCTFabricComponentsPlugins.h" +#import "RNLiveMarkdownModule.h" using namespace facebook::react; @@ -38,11 +39,18 @@ - (instancetype)initWithFrame:(CGRect)frame - (void)updateState:(const facebook::react::State::Shared &)state oldState:(const facebook::react::State::Shared &)oldState { auto data = std::static_pointer_cast(state)->getData(); + + if (_decoratorFamily != nullptr) { + [RNLiveMarkdownModule unregisterFamilyForUpdates:_decoratorFamily]; + } + _decoratorFamily = data.decoratorFamily; + [RNLiveMarkdownModule registerFamilyForUpdates:_decoratorFamily]; } - (void)willMoveToSuperview:(UIView *)newSuperview { if (newSuperview == nil) { + [RNLiveMarkdownModule unregisterFamilyForUpdates:_decoratorFamily]; _decoratorFamily = nullptr; } diff --git a/ios/RNLiveMarkdownModule.h b/ios/RNLiveMarkdownModule.h index 205f6182..57695a03 100644 --- a/ios/RNLiveMarkdownModule.h +++ b/ios/RNLiveMarkdownModule.h @@ -7,6 +7,10 @@ @interface RNLiveMarkdownModule : RCTEventEmitter ++ (void) registerFamilyForUpdates:(facebook::react::ShadowNodeFamily::Shared) family; ++ (void) unregisterFamilyForUpdates:(facebook::react::ShadowNodeFamily::Shared) family; ++ (void) runForEveryFamily:(std::function)fun; + @end #endif // RCT_NEW_ARCH_ENABLED diff --git a/ios/RNLiveMarkdownModule.mm b/ios/RNLiveMarkdownModule.mm index aa5016c2..9adcab99 100644 --- a/ios/RNLiveMarkdownModule.mm +++ b/ios/RNLiveMarkdownModule.mm @@ -11,6 +11,8 @@ @implementation RNLiveMarkdownModule { std::shared_ptr commitHook_; } +static std::set _familiesToUpdate; + RCT_EXPORT_MODULE(@"RNLiveMarkdownModule") - (NSNumber*)install @@ -32,6 +34,38 @@ - (NSNumber*)install return std::make_shared(params); } +- (void)invalidate +{ + @synchronized (self) { + _familiesToUpdate.clear(); + } + + [super invalidate]; +} + ++ (void) registerFamilyForUpdates:(facebook::react::ShadowNodeFamily::Shared)family +{ + @synchronized (self) { + _familiesToUpdate.insert(family); + } +} + ++ (void)unregisterFamilyForUpdates:(facebook::react::ShadowNodeFamily::Shared)family +{ + @synchronized (self) { + _familiesToUpdate.erase(family); + } +} + ++ (void)runForEveryFamily:(std::function)fun +{ + @synchronized (self) { + for (auto &family : _familiesToUpdate) { + fun(family); + } + } +} + @end #endif // RCT_NEW_ARCH_ENABLED From 357eb6297938eda43c185dcaac168ffe551e58cc Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Thu, 22 Feb 2024 11:17:46 +0100 Subject: [PATCH 08/35] Always pass default text attributes --- ios/MarkdownCommitHook.mm | 11 +- ios/MarkdownTextInputDecoratorView.mm | 2 +- ...TBackedTextFieldDelegateAdapter+Markdown.m | 2 +- ios/RCTBaseTextInputView+Markdown.m | 4 +- ios/RCTMarkdownUtils.h | 3 - ios/RCTMarkdownUtils.mm | 237 ++++++++---------- ios/RCTTextInputComponentView+Markdown.mm | 14 +- ios/RCTUITextView+Markdown.mm | 2 +- 8 files changed, 131 insertions(+), 144 deletions(-) diff --git a/ios/MarkdownCommitHook.mm b/ios/MarkdownCommitHook.mm index e2fd869d..c76548a1 100644 --- a/ios/MarkdownCommitHook.mm +++ b/ios/MarkdownCommitHook.mm @@ -49,18 +49,23 @@ }]; for (auto &nodes : nodesToUpdate) { - auto tag = nodes.textInput->getTag(); rootNode = rootNode->cloneTree(nodes.textInput->getFamily(), [&nodes](const ShadowNode& node){ const auto &textInputState = *std::static_pointer_cast>(nodes.textInput->getState()); const auto &stateData = textInputState.getData(); + if (stateData.attributedStringBox.getMode() == AttributedStringBox::Mode::Value) { const auto &markdownProps = *std::static_pointer_cast(nodes.decorator->getProps()); + const auto &textInputProps = *std::static_pointer_cast(nodes.textInput->getProps()); + + const auto defaultTextAttributes = + RCTNSTextAttributesFromTextAttributes(textInputProps.getEffectiveTextAttributes(RCTFontSizeMultiplier())); + RCTMarkdownStyle *markdownStyle = [[RCTMarkdownStyle alloc] initWithStruct:markdownProps.markdownStyle]; - RCTMarkdownUtils *utils = [[RCTMarkdownUtils alloc] initWithBackedTextInputView:nil]; + RCTMarkdownUtils *utils = [[RCTMarkdownUtils alloc] init]; [utils setMarkdownStyle:markdownStyle]; auto nsAttributedString = RCTNSAttributedStringFromAttributedStringBox(stateData.attributedStringBox); - auto newString = [utils parseMarkdownWithPreviousAttributes:nsAttributedString]; + auto newString = [utils parseMarkdown:nsAttributedString withAttributes:defaultTextAttributes]; auto newStateData = std::make_shared(stateData); newStateData->attributedStringBox = RCTAttributedStringBoxFromNSAttributedString(newString); diff --git a/ios/MarkdownTextInputDecoratorView.mm b/ios/MarkdownTextInputDecoratorView.mm index c8c1228d..4ca1906f 100644 --- a/ios/MarkdownTextInputDecoratorView.mm +++ b/ios/MarkdownTextInputDecoratorView.mm @@ -58,7 +58,7 @@ - (void)didMoveToWindow { UIView *backedTextInputView = _textInput.backedTextInputView; #endif /* RCT_NEW_ARCH_ENABLED */ - _markdownUtils = [[RCTMarkdownUtils alloc] initWithBackedTextInputView:backedTextInputView]; + _markdownUtils = [[RCTMarkdownUtils alloc] init]; react_native_assert(_markdownStyle != nil); [_markdownUtils setMarkdownStyle:_markdownStyle]; diff --git a/ios/RCTBackedTextFieldDelegateAdapter+Markdown.m b/ios/RCTBackedTextFieldDelegateAdapter+Markdown.m index bb3423b6..11c3baf8 100644 --- a/ios/RCTBackedTextFieldDelegateAdapter+Markdown.m +++ b/ios/RCTBackedTextFieldDelegateAdapter+Markdown.m @@ -19,7 +19,7 @@ - (void)markdown_textFieldDidChange if (markdownUtils != nil) { RCTUITextField *backedTextInputView = [self valueForKey:@"_backedTextInputView"]; UITextRange *range = backedTextInputView.selectedTextRange; - backedTextInputView.attributedText = [markdownUtils parseMarkdown:backedTextInputView.attributedText]; + backedTextInputView.attributedText = [markdownUtils parseMarkdown:backedTextInputView.attributedText withAttributes:backedTextInputView.defaultTextAttributes]; [backedTextInputView setSelectedTextRange:range notifyDelegate:YES]; } diff --git a/ios/RCTBaseTextInputView+Markdown.m b/ios/RCTBaseTextInputView+Markdown.m index ad254869..7fb82a77 100644 --- a/ios/RCTBaseTextInputView+Markdown.m +++ b/ios/RCTBaseTextInputView+Markdown.m @@ -16,7 +16,7 @@ - (void)markdown_setAttributedText:(NSAttributedString *)attributedText { RCTMarkdownUtils *markdownUtils = [self getMarkdownUtils]; if (markdownUtils != nil) { - attributedText = [markdownUtils parseMarkdown:attributedText]; + attributedText = [markdownUtils parseMarkdown:attributedText withAttributes:self.backedTextInputView.defaultTextAttributes]; } // Call the original method @@ -28,7 +28,7 @@ - (void)markdown_updateLocalData RCTMarkdownUtils *markdownUtils = [self getMarkdownUtils]; if (markdownUtils != nil) { UITextRange *range = self.backedTextInputView.selectedTextRange; - NSAttributedString *attributedText = [markdownUtils parseMarkdown:self.backedTextInputView.attributedText]; + NSAttributedString *attributedText = [markdownUtils parseMarkdown:self.backedTextInputView.attributedText withAttributes:self.backedTextInputView.defaultTextAttributes]; [self.backedTextInputView setAttributedText:attributedText]; [self.backedTextInputView setSelectedTextRange:range notifyDelegate:YES]; } diff --git a/ios/RCTMarkdownUtils.h b/ios/RCTMarkdownUtils.h index 9217748a..d64f4378 100644 --- a/ios/RCTMarkdownUtils.h +++ b/ios/RCTMarkdownUtils.h @@ -9,9 +9,6 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) NSMutableArray *blockquoteRanges; @property (weak, nonatomic) UIView *backedTextInputView; -- (instancetype)initWithBackedTextInputView:(UIView *)backedTextInputView; - -- (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input; - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withAttributes:(nullable NSDictionary*)attributes; @end diff --git a/ios/RCTMarkdownUtils.mm b/ios/RCTMarkdownUtils.mm index 1344cd0e..9bc9437c 100644 --- a/ios/RCTMarkdownUtils.mm +++ b/ios/RCTMarkdownUtils.mm @@ -11,137 +11,116 @@ @implementation RCTMarkdownUtils { __weak RCTMarkdownStyle *_prevMarkdownStyle; } -- (instancetype)initWithBackedTextInputView:(UIView *)backedTextInputView +- (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withAttributes:(nullable NSDictionary *)attributes { - if (self = [super init]) { - _backedTextInputView = backedTextInputView; - } - return self; -} - -- (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input -{ - RCTAssertMainQueue(); - - if (input == nil) { - return nil; - } - - NSString *inputString = [input string]; - if ([inputString isEqualToString:_prevInputString] && [_backedTextInputView.defaultTextAttributes isEqualToDictionary:_prevTextAttributes] && [_markdownStyle isEqual:_prevMarkdownStyle]) { - return _prevAttributedString; - } - - NSAttributedString *attributedString = [self parseMarkdown:input withAttributes:_backedTextInputView.defaultTextAttributes]; - - _prevInputString = inputString; - _prevAttributedString = attributedString; - _prevTextAttributes = _backedTextInputView.defaultTextAttributes; - _prevMarkdownStyle = _markdownStyle; - - return attributedString; -} - -- (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withAttributes:(nullable NSDictionary*)attributes -{ - if (input == nil) { - return nil; - } - - NSString *inputString = [input string]; - - 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:_backedTextInputView.defaultTextAttributes]; - [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)]; - - _blockquoteRanges = [NSMutableArray new]; - - [ranges enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - NSArray *item = obj; - NSString *type = item[0]; - NSUInteger location = [item[1] unsignedIntegerValue]; - NSUInteger length = [item[2] unsignedIntegerValue]; - NSRange range = NSMakeRange(location, length); - - if ([type isEqualToString:@"bold"] || [type isEqualToString:@"italic"] || [type isEqualToString:@"code"] || [type isEqualToString:@"pre"] || [type isEqualToString:@"h1"]) { - 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]; - } else if ([type isEqualToString:@"pre"]) { - font = [RCTFont updateFont:font withFamily:_markdownStyle.preFontFamily]; - } else if ([type isEqualToString:@"h1"]) { - font = [RCTFont updateFont:font withFamily:nil - size:[NSNumber numberWithFloat:_markdownStyle.h1FontSize] - weight:@"bold" - style:nil - variant:nil - scaleMultiplier:0]; + @synchronized (self) { + if (input == nil) { + return nil; } - [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:@"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; - NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; - paragraphStyle.firstLineHeadIndent = indent; - paragraphStyle.headIndent = indent; - [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range]; - [_blockquoteRanges addObject:[NSValue valueWithRange:range]]; - } 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]; - } - }]; - - [attributedString endEditing]; - - return attributedString; + + 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)]; + + _blockquoteRanges = [NSMutableArray new]; + + [ranges enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + NSArray *item = obj; + NSString *type = item[0]; + NSUInteger location = [item[1] unsignedIntegerValue]; + NSUInteger length = [item[2] unsignedIntegerValue]; + NSRange range = NSMakeRange(location, length); + + if ([type isEqualToString:@"bold"] || [type isEqualToString:@"italic"] || [type isEqualToString:@"code"] || [type isEqualToString:@"pre"] || [type isEqualToString:@"h1"]) { + 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]; + } else if ([type isEqualToString:@"pre"]) { + font = [RCTFont updateFont:font withFamily:_markdownStyle.preFontFamily]; + } else if ([type isEqualToString:@"h1"]) { + font = [RCTFont updateFont:font withFamily:nil + size:[NSNumber numberWithFloat:_markdownStyle.h1FontSize] + weight:@"bold" + 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:@"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; + NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; + paragraphStyle.firstLineHeadIndent = indent; + paragraphStyle.headIndent = indent; + [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range]; + [_blockquoteRanges addObject:[NSValue valueWithRange:range]]; + } 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]; + } + }]; + + [attributedString endEditing]; + + _prevInputString = inputString; + _prevAttributedString = attributedString; + _prevTextAttributes = attributes; + _prevMarkdownStyle = _markdownStyle; + + return attributedString; + } } @end diff --git a/ios/RCTTextInputComponentView+Markdown.mm b/ios/RCTTextInputComponentView+Markdown.mm index f729ce61..7d05bf4a 100644 --- a/ios/RCTTextInputComponentView+Markdown.mm +++ b/ios/RCTTextInputComponentView+Markdown.mm @@ -13,8 +13,8 @@ - (void)setMarkdownUtils:(RCTMarkdownUtils *)markdownUtils { if (markdownUtils != nil) { // force Markdown formatting on first render because `_setAttributedText` is called before `setMarkdownUtils` - RCTUITextField *backedTextInputView = [self valueForKey:@"_backedTextInputView"]; - backedTextInputView.attributedText = [markdownUtils parseMarkdown:backedTextInputView.attributedText]; + RCTUITextField *backedTextInputView = [self getBackedTextInputView]; + backedTextInputView.attributedText = [markdownUtils parseMarkdown:backedTextInputView.attributedText withAttributes:backedTextInputView.defaultTextAttributes]; } } @@ -22,11 +22,17 @@ - (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]; - if (markdownUtils != nil) { - attributedString = [markdownUtils parseMarkdown:attributedString]; + RCTUITextField *backedTextInputView = [self getBackedTextInputView]; + if (markdownUtils != nil && backedTextInputView != nil) { + attributedString = [markdownUtils parseMarkdown:attributedString withAttributes:backedTextInputView.defaultTextAttributes]; } // Call the original method diff --git a/ios/RCTUITextView+Markdown.mm b/ios/RCTUITextView+Markdown.mm index dd08b87b..70f2d882 100644 --- a/ios/RCTUITextView+Markdown.mm +++ b/ios/RCTUITextView+Markdown.mm @@ -17,7 +17,7 @@ - (void)markdown_textDidChange RCTMarkdownUtils *markdownUtils = [self getMarkdownUtils]; if (markdownUtils != nil) { UITextRange *range = self.selectedTextRange; - super.attributedText = [markdownUtils parseMarkdown:self.attributedText]; + 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 } From e4f7e77b272aa4ee3a171444543e533abfee441c Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Thu, 22 Feb 2024 14:20:13 +0100 Subject: [PATCH 09/35] Move shadow family registry to another file --- ios/MarkdownCommitHook.mm | 9 +++--- ios/MarkdownShadowFamilyRegistry.cpp | 29 +++++++++++++++++ ios/MarkdownShadowFamilyRegistry.h | 15 +++++++++ ...MarkdownTextInputDecoratorComponentView.mm | 10 +++--- ios/RNLiveMarkdownModule.h | 5 --- ios/RNLiveMarkdownModule.mm | 31 ++----------------- 6 files changed, 56 insertions(+), 43 deletions(-) create mode 100644 ios/MarkdownShadowFamilyRegistry.cpp create mode 100644 ios/MarkdownShadowFamilyRegistry.h diff --git a/ios/MarkdownCommitHook.mm b/ios/MarkdownCommitHook.mm index c76548a1..aed7bc50 100644 --- a/ios/MarkdownCommitHook.mm +++ b/ios/MarkdownCommitHook.mm @@ -2,12 +2,13 @@ #ifdef RCT_NEW_ARCH_ENABLED #include -#import +#include +#include #include "MarkdownCommitHook.h" #include "RCTMarkdownStyle.h" #include "RCTMarkdownUtils.h" -#include "RNLiveMarkdownModule.h" +#include "MarkdownShadowFamilyRegistry.h" using namespace facebook::react; @@ -29,7 +30,7 @@ std::vector nodesToUpdate; - [RNLiveMarkdownModule runForEveryFamily:[&rootNode, &nodesToUpdate](ShadowNodeFamily::Shared family) { + MarkdownShadowFamilyRegistry::runForEveryFamily([&rootNode, &nodesToUpdate](ShadowNodeFamily::Shared family) { auto ancestors = family->getAncestors(*rootNode); if (!ancestors.empty()) { @@ -46,7 +47,7 @@ }); } } - }]; + }); for (auto &nodes : nodesToUpdate) { rootNode = rootNode->cloneTree(nodes.textInput->getFamily(), [&nodes](const ShadowNode& node){ diff --git a/ios/MarkdownShadowFamilyRegistry.cpp b/ios/MarkdownShadowFamilyRegistry.cpp new file mode 100644 index 00000000..848f7726 --- /dev/null +++ b/ios/MarkdownShadowFamilyRegistry.cpp @@ -0,0 +1,29 @@ +#include "MarkdownShadowFamilyRegistry.h" + +std::set MarkdownShadowFamilyRegistry::_familiesToUpdate; +std::mutex MarkdownShadowFamilyRegistry::_familiesMutex; + +void MarkdownShadowFamilyRegistry::registerFamilyForUpdates(facebook::react::ShadowNodeFamily::Shared family) +{ + auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::_familiesMutex); + MarkdownShadowFamilyRegistry::_familiesToUpdate.insert(family); +} + +void MarkdownShadowFamilyRegistry::unregisterFamilyForUpdates(facebook::react::ShadowNodeFamily::Shared family) +{ + auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::_familiesMutex); + MarkdownShadowFamilyRegistry::_familiesToUpdate.erase(family); +} + +void MarkdownShadowFamilyRegistry::clearRegisteredFamilies() { + auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::_familiesMutex); + MarkdownShadowFamilyRegistry::_familiesToUpdate.clear(); +} + +void MarkdownShadowFamilyRegistry::runForEveryFamily(std::function fun) +{ + auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::_familiesMutex); + for (auto &family : MarkdownShadowFamilyRegistry::_familiesToUpdate) { + fun(family); + } +} diff --git a/ios/MarkdownShadowFamilyRegistry.h b/ios/MarkdownShadowFamilyRegistry.h new file mode 100644 index 00000000..b00cb879 --- /dev/null +++ b/ios/MarkdownShadowFamilyRegistry.h @@ -0,0 +1,15 @@ +#include + +#include + +class MarkdownShadowFamilyRegistry { +public: + static void registerFamilyForUpdates(facebook::react::ShadowNodeFamily::Shared family); + static void unregisterFamilyForUpdates(facebook::react::ShadowNodeFamily::Shared family); + static void clearRegisteredFamilies(); + static void runForEveryFamily(std::function fun); + +private: + static std::set _familiesToUpdate; + static std::mutex _familiesMutex; +}; diff --git a/ios/MarkdownTextInputDecoratorComponentView.mm b/ios/MarkdownTextInputDecoratorComponentView.mm index b39dd4e2..728a6e9e 100644 --- a/ios/MarkdownTextInputDecoratorComponentView.mm +++ b/ios/MarkdownTextInputDecoratorComponentView.mm @@ -1,14 +1,14 @@ // This guard prevent this file to be compiled in the old architecture. #ifdef RCT_NEW_ARCH_ENABLED -#import "MarkdownTextInputDecoratorComponentDescriptor.h" #import #import #import #import +#import "MarkdownTextInputDecoratorComponentDescriptor.h" +#import "MarkdownShadowFamilyRegistry.h" #import "RCTFabricComponentsPlugins.h" -#import "RNLiveMarkdownModule.h" using namespace facebook::react; @@ -41,16 +41,16 @@ - (void)updateState:(const facebook::react::State::Shared &)state oldState:(cons auto data = std::static_pointer_cast(state)->getData(); if (_decoratorFamily != nullptr) { - [RNLiveMarkdownModule unregisterFamilyForUpdates:_decoratorFamily]; + MarkdownShadowFamilyRegistry::unregisterFamilyForUpdates(_decoratorFamily); } _decoratorFamily = data.decoratorFamily; - [RNLiveMarkdownModule registerFamilyForUpdates:_decoratorFamily]; + MarkdownShadowFamilyRegistry::registerFamilyForUpdates(_decoratorFamily); } - (void)willMoveToSuperview:(UIView *)newSuperview { if (newSuperview == nil) { - [RNLiveMarkdownModule unregisterFamilyForUpdates:_decoratorFamily]; + MarkdownShadowFamilyRegistry::unregisterFamilyForUpdates(_decoratorFamily); _decoratorFamily = nullptr; } diff --git a/ios/RNLiveMarkdownModule.h b/ios/RNLiveMarkdownModule.h index 57695a03..9a40935e 100644 --- a/ios/RNLiveMarkdownModule.h +++ b/ios/RNLiveMarkdownModule.h @@ -6,11 +6,6 @@ @interface RNLiveMarkdownModule : RCTEventEmitter - -+ (void) registerFamilyForUpdates:(facebook::react::ShadowNodeFamily::Shared) family; -+ (void) unregisterFamilyForUpdates:(facebook::react::ShadowNodeFamily::Shared) family; -+ (void) runForEveryFamily:(std::function)fun; - @end #endif // RCT_NEW_ARCH_ENABLED diff --git a/ios/RNLiveMarkdownModule.mm b/ios/RNLiveMarkdownModule.mm index 9adcab99..a9b36f95 100644 --- a/ios/RNLiveMarkdownModule.mm +++ b/ios/RNLiveMarkdownModule.mm @@ -5,14 +5,13 @@ #import "RNLiveMarkdownModule.h" #import "MarkdownCommitHook.h" +#import "MarkdownShadowFamilyRegistry.h" @implementation RNLiveMarkdownModule { BOOL installed_; std::shared_ptr commitHook_; } -static std::set _familiesToUpdate; - RCT_EXPORT_MODULE(@"RNLiveMarkdownModule") - (NSNumber*)install @@ -36,36 +35,10 @@ - (NSNumber*)install - (void)invalidate { - @synchronized (self) { - _familiesToUpdate.clear(); - } - + MarkdownShadowFamilyRegistry::clearRegisteredFamilies(); [super invalidate]; } -+ (void) registerFamilyForUpdates:(facebook::react::ShadowNodeFamily::Shared)family -{ - @synchronized (self) { - _familiesToUpdate.insert(family); - } -} - -+ (void)unregisterFamilyForUpdates:(facebook::react::ShadowNodeFamily::Shared)family -{ - @synchronized (self) { - _familiesToUpdate.erase(family); - } -} - -+ (void)runForEveryFamily:(std::function)fun -{ - @synchronized (self) { - for (auto &family : _familiesToUpdate) { - fun(family); - } - } -} - @end #endif // RCT_NEW_ARCH_ENABLED From 0e1d64220931bd83d3b49620ae1d812f03732885 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Thu, 22 Feb 2024 16:00:43 +0100 Subject: [PATCH 10/35] Fix first render --- ios/MarkdownCommitHook.mm | 26 +++++++++++++++++--- ios/MarkdownTextInputDecoratorShadowNode.cpp | 7 ++++-- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/ios/MarkdownCommitHook.mm b/ios/MarkdownCommitHook.mm index aed7bc50..de5841b4 100644 --- a/ios/MarkdownCommitHook.mm +++ b/ios/MarkdownCommitHook.mm @@ -58,15 +58,31 @@ const auto &markdownProps = *std::static_pointer_cast(nodes.decorator->getProps()); const auto &textInputProps = *std::static_pointer_cast(nodes.textInput->getProps()); - const auto defaultTextAttributes = - RCTNSTextAttributesFromTextAttributes(textInputProps.getEffectiveTextAttributes(RCTFontSizeMultiplier())); + const auto defaultTextAttributes = textInputProps.getEffectiveTextAttributes(RCTFontSizeMultiplier()); + const auto defaultNSTextAttributes = RCTNSTextAttributesFromTextAttributes(defaultTextAttributes); RCTMarkdownStyle *markdownStyle = [[RCTMarkdownStyle alloc] initWithStruct:markdownProps.markdownStyle]; RCTMarkdownUtils *utils = [[RCTMarkdownUtils alloc] init]; [utils setMarkdownStyle:markdownStyle]; auto nsAttributedString = RCTNSAttributedStringFromAttributedStringBox(stateData.attributedStringBox); - auto newString = [utils parseMarkdown:nsAttributedString withAttributes:defaultTextAttributes]; + + auto plainString = std::string([[nsAttributedString string] UTF8String]); + + if (plainString != textInputProps.text) { + auto attributedString = AttributedString{}; + + attributedString.appendFragment( + AttributedString::Fragment{textInputProps.text, defaultTextAttributes}); + + auto attachments = BaseTextShadowNode::Attachments{}; + BaseTextShadowNode::buildAttributedString( + defaultTextAttributes, *nodes.textInput, attributedString, attachments); + + nsAttributedString = RCTNSAttributedStringFromAttributedStringBox(AttributedStringBox{attributedString}); + } + + auto newString = [utils parseMarkdown:nsAttributedString withAttributes:defaultNSTextAttributes]; auto newStateData = std::make_shared(stateData); newStateData->attributedStringBox = RCTAttributedStringBoxFromNSAttributedString(newString); @@ -76,7 +92,9 @@ }); } - return node.clone({}); + return node.clone({ + .state = node.getState() + }); }); } diff --git a/ios/MarkdownTextInputDecoratorShadowNode.cpp b/ios/MarkdownTextInputDecoratorShadowNode.cpp index e52ee1db..6eaa27ab 100644 --- a/ios/MarkdownTextInputDecoratorShadowNode.cpp +++ b/ios/MarkdownTextInputDecoratorShadowNode.cpp @@ -1,7 +1,8 @@ -#include "MarkdownTextInputDecoratorShadowNode.h" - #include +#include "MarkdownTextInputDecoratorShadowNode.h" +#include "MarkdownShadowFamilyRegistry.h" + namespace facebook { namespace react { @@ -10,6 +11,8 @@ extern const char MarkdownTextInputDecoratorViewComponentName[] = "MarkdownTextI const ShadowNodeFragment::Value MarkdownTextInputDecoratorShadowNode::updateFragmentState(ShadowNodeFragment const &fragment, ShadowNodeFamily::Shared const &family) { const auto newStateData = std::make_shared(family); + MarkdownShadowFamilyRegistry::registerFamilyForUpdates(family); + return ShadowNodeFragment::Value({ .props = fragment.props, .children = fragment.children, From fc8373272711dcc3910a8fbfe9f6ef645e052e00 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Thu, 22 Feb 2024 16:05:29 +0100 Subject: [PATCH 11/35] Change RN prefix to RCT --- ios/{RNLiveMarkdownModule.h => RCTLiveMarkdownModule.h} | 2 +- ios/{RNLiveMarkdownModule.mm => RCTLiveMarkdownModule.mm} | 4 ++-- src/NativeMarkdownModule.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename ios/{RNLiveMarkdownModule.h => RCTLiveMarkdownModule.h} (70%) rename ios/{RNLiveMarkdownModule.mm => RCTLiveMarkdownModule.mm} (93%) diff --git a/ios/RNLiveMarkdownModule.h b/ios/RCTLiveMarkdownModule.h similarity index 70% rename from ios/RNLiveMarkdownModule.h rename to ios/RCTLiveMarkdownModule.h index 9a40935e..c01ca5ec 100644 --- a/ios/RNLiveMarkdownModule.h +++ b/ios/RCTLiveMarkdownModule.h @@ -5,7 +5,7 @@ #import -@interface RNLiveMarkdownModule : RCTEventEmitter +@interface RCTLiveMarkdownModule : RCTEventEmitter @end #endif // RCT_NEW_ARCH_ENABLED diff --git a/ios/RNLiveMarkdownModule.mm b/ios/RCTLiveMarkdownModule.mm similarity index 93% rename from ios/RNLiveMarkdownModule.mm rename to ios/RCTLiveMarkdownModule.mm index a9b36f95..87e368c2 100644 --- a/ios/RNLiveMarkdownModule.mm +++ b/ios/RCTLiveMarkdownModule.mm @@ -3,11 +3,11 @@ #import #import -#import "RNLiveMarkdownModule.h" +#import "RCTLiveMarkdownModule.h" #import "MarkdownCommitHook.h" #import "MarkdownShadowFamilyRegistry.h" -@implementation RNLiveMarkdownModule { +@implementation RCTLiveMarkdownModule { BOOL installed_; std::shared_ptr commitHook_; } diff --git a/src/NativeMarkdownModule.ts b/src/NativeMarkdownModule.ts index 4a8328f1..6a6c2649 100644 --- a/src/NativeMarkdownModule.ts +++ b/src/NativeMarkdownModule.ts @@ -5,4 +5,4 @@ interface Spec extends TurboModule { install: () => boolean; } -export default TurboModuleRegistry.get('RNLiveMarkdownModule'); +export default TurboModuleRegistry.get('RCTLiveMarkdownModule'); From 1262c802864d7653e14f7e5a12e0fc3ad29c0e75 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Thu, 22 Feb 2024 17:09:58 +0100 Subject: [PATCH 12/35] Add comments and minor changes --- ios/MarkdownCommitHook.h | 2 +- ios/MarkdownCommitHook.mm | 58 +++++++++++++++----- ios/MarkdownShadowFamilyRegistry.h | 6 ++ ios/MarkdownTextInputDecoratorShadowNode.cpp | 3 + ios/RCTLiveMarkdownModule.h | 2 +- ios/RCTLiveMarkdownModule.mm | 3 + 6 files changed, 59 insertions(+), 15 deletions(-) diff --git a/ios/MarkdownCommitHook.h b/ios/MarkdownCommitHook.h index 818c48c3..ba5d5478 100644 --- a/ios/MarkdownCommitHook.h +++ b/ios/MarkdownCommitHook.h @@ -13,7 +13,7 @@ using namespace facebook::react; namespace livemarkdown { -struct MarkdownTextInputNode { +struct MarkdownTextInputDecoratorPair { std::shared_ptr textInput; std::shared_ptr decorator; }; diff --git a/ios/MarkdownCommitHook.mm b/ios/MarkdownCommitHook.mm index de5841b4..d610b047 100644 --- a/ios/MarkdownCommitHook.mm +++ b/ios/MarkdownCommitHook.mm @@ -28,9 +28,28 @@ RootShadowNode::Unshared const &newRootShadowNode) const noexcept { auto rootNode = newRootShadowNode->ShadowNode::clone(ShadowNodeFragment{}); - std::vector nodesToUpdate; + // A preface to why we do the weird thing below: + // On the new architecture there are two ways of measuring text on iOS: by value and by pointer. + // When done by value, the attributed string to be measured is created on the c++ side. We cannot + // modify this process as we do not extend TextInputShadowNode. We also cannot really change the + // layout manager used to do this, since it's a private field (ok, we can but in a not very nice way). + // But also, the logic for parsing and applying markdown is written in JS/ObjC and we really wouldn't + // want to reimplement it in c++. + // + // Nice thing is that it can also be done by pointer to NSAttributedString, which is the platform's + // way to handle styled text, and is also used by Live Markdown. On this path, the measurement is done + // by the OS APIs. The thing we want to make sure of, is that markdown-decorated text input always uses + // this path and uses a pointer to a string with markdown styles applied. Thankfully, RN provides nice + // utility functions that allow to convert between the RN's AttributedString and iOS's NSAttributedString. + // The logic below does exactly that. + // In order to properly apply markdown formatting to the text input, we need to update the TextInputShadowNode's + // state with styled string, but we only have access to the ShadowNodeFamilies of the decorator components. + // We also know that a markdown decorator is always preceded with the TextInput to decorate, so we need to take + // the sibling. + std::vector nodesToUpdate; MarkdownShadowFamilyRegistry::runForEveryFamily([&rootNode, &nodesToUpdate](ShadowNodeFamily::Shared family) { + // get the path from the root to the node from the decorator family auto ancestors = family->getAncestors(*rootNode); if (!ancestors.empty()) { @@ -38,9 +57,12 @@ auto index = ancestors.back().second; auto markdownNode = parentNode.getChildren().at(index); + // text input always precedes the decorator component auto previousSibling = parentNode.getChildren().at(index - 1); if (auto textInputNode = std::dynamic_pointer_cast(previousSibling)) { + // store the pair of text input and decorator to update in the next step + // we need both, decorator to get markdown style and text input to update it nodesToUpdate.push_back({ textInputNode, std::dynamic_pointer_cast(markdownNode), @@ -50,28 +72,38 @@ }); for (auto &nodes : nodesToUpdate) { - rootNode = rootNode->cloneTree(nodes.textInput->getFamily(), [&nodes](const ShadowNode& node){ - const auto &textInputState = *std::static_pointer_cast>(nodes.textInput->getState()); - const auto &stateData = textInputState.getData(); - - if (stateData.attributedStringBox.getMode() == AttributedStringBox::Mode::Value) { + const auto &textInputState = *std::static_pointer_cast>(nodes.textInput->getState()); + const auto &stateData = textInputState.getData(); + + // We only want to update the shadow node when the attributed string is stored by value + // If it's stored by pointer, the markdown formatting should already by applied to it, since the + // only source of that pointer (besides this commit hook) is RCTTextInputComponentView, which + // has the relevant method swizzled to make sure the markdown styles are always applied before + // updating state + if (stateData.attributedStringBox.getMode() == AttributedStringBox::Mode::Value) { + rootNode = rootNode->cloneTree(nodes.textInput->getFamily(), [&nodes, &textInputState, &stateData](const ShadowNode& node) { const auto &markdownProps = *std::static_pointer_cast(nodes.decorator->getProps()); const auto &textInputProps = *std::static_pointer_cast(nodes.textInput->getProps()); const auto defaultTextAttributes = textInputProps.getEffectiveTextAttributes(RCTFontSizeMultiplier()); const auto defaultNSTextAttributes = RCTNSTextAttributesFromTextAttributes(defaultTextAttributes); + // this can possibly be optimized RCTMarkdownStyle *markdownStyle = [[RCTMarkdownStyle alloc] initWithStruct:markdownProps.markdownStyle]; RCTMarkdownUtils *utils = [[RCTMarkdownUtils alloc] init]; [utils setMarkdownStyle:markdownStyle]; + // convert the attibuted string stored in state to NSAttributedString auto nsAttributedString = RCTNSAttributedStringFromAttributedStringBox(stateData.attributedStringBox); + // Handles the first render, where the text stored in props is different than the one stored in state + // The one in state is empty, while the one in props is passed from JS + // If we don't update the state here, we'll end up with a one-default-line-sized text input. + // A better condition to do that can be probably chosen, but this seems to work auto plainString = std::string([[nsAttributedString string] UTF8String]); - if (plainString != textInputProps.text) { + // creates new AttributedString from props, adapted from TextInputShadowNode (ios one, text inputs are platform-specific) auto attributedString = AttributedString{}; - attributedString.appendFragment( AttributedString::Fragment{textInputProps.text, defaultTextAttributes}); @@ -79,23 +111,23 @@ BaseTextShadowNode::buildAttributedString( defaultTextAttributes, *nodes.textInput, attributedString, attachments); + // convert the newly created attributed string to NSAttributedString nsAttributedString = RCTNSAttributedStringFromAttributedStringBox(AttributedStringBox{attributedString}); } + // apply markdown auto newString = [utils parseMarkdown:nsAttributedString withAttributes:defaultNSTextAttributes]; + // create a clone of the old TextInputState and update the attributed string box to point to the string with markdown applied auto newStateData = std::make_shared(stateData); newStateData->attributedStringBox = RCTAttributedStringBoxFromNSAttributedString(newString); + // clone the text input with the new state return node.clone({ .state = std::make_shared>(newStateData, textInputState), }); - } - - return node.clone({ - .state = node.getState() }); - }); + } } return std::static_pointer_cast(rootNode); diff --git a/ios/MarkdownShadowFamilyRegistry.h b/ios/MarkdownShadowFamilyRegistry.h index b00cb879..df4aa554 100644 --- a/ios/MarkdownShadowFamilyRegistry.h +++ b/ios/MarkdownShadowFamilyRegistry.h @@ -2,6 +2,12 @@ #include +// A registry to store pointers to the ShadowNodeFamilies of markdown decorators. +// The only place we can _legally_ access the family of shadow node is in the constructor +// and we need it inside commit hook. To achieve it, we use this simple registry where families +// are registered when nodes are created and cleaned up when native view is removed from window +// or when a turbomodule is deallocated. + class MarkdownShadowFamilyRegistry { public: static void registerFamilyForUpdates(facebook::react::ShadowNodeFamily::Shared family); diff --git a/ios/MarkdownTextInputDecoratorShadowNode.cpp b/ios/MarkdownTextInputDecoratorShadowNode.cpp index 6eaa27ab..1e94bc76 100644 --- a/ios/MarkdownTextInputDecoratorShadowNode.cpp +++ b/ios/MarkdownTextInputDecoratorShadowNode.cpp @@ -13,6 +13,9 @@ const ShadowNodeFragment::Value MarkdownTextInputDecoratorShadowNode::updateFrag MarkdownShadowFamilyRegistry::registerFamilyForUpdates(family); + // we pass the pointer to the ShadowNodeFamily in the initial state, so it's propagated on every clone + // we need it to clear the reference in the registry when the view is removed from window + // it cannot be done in the destructor, as multiple shadow nodes for the same family may be created return ShadowNodeFragment::Value({ .props = fragment.props, .children = fragment.children, diff --git a/ios/RCTLiveMarkdownModule.h b/ios/RCTLiveMarkdownModule.h index c01ca5ec..b175a0b2 100644 --- a/ios/RCTLiveMarkdownModule.h +++ b/ios/RCTLiveMarkdownModule.h @@ -4,7 +4,7 @@ #import #import - +// Without inheriting after RCTEventEmitter we don't get access to bridge @interface RCTLiveMarkdownModule : RCTEventEmitter @end diff --git a/ios/RCTLiveMarkdownModule.mm b/ios/RCTLiveMarkdownModule.mm index 87e368c2..4af759c5 100644 --- a/ios/RCTLiveMarkdownModule.mm +++ b/ios/RCTLiveMarkdownModule.mm @@ -7,6 +7,9 @@ #import "MarkdownCommitHook.h" #import "MarkdownShadowFamilyRegistry.h" +// A turbomodule used to register the commit hook +// I think this is the easiest way to access the UIManager, which we need to actually register the hook + @implementation RCTLiveMarkdownModule { BOOL installed_; std::shared_ptr commitHook_; From acd43a3d77e10b2af4f2fa59f791ad406731dc96 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Mon, 26 Feb 2024 15:06:33 +0100 Subject: [PATCH 13/35] Fix types for 0.73 --- ios/MarkdownCommitHook.h | 6 +++--- ios/MarkdownCommitHook.mm | 2 +- ios/MarkdownShadowFamilyRegistry.h | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ios/MarkdownCommitHook.h b/ios/MarkdownCommitHook.h index ba5d5478..450f02c5 100644 --- a/ios/MarkdownCommitHook.h +++ b/ios/MarkdownCommitHook.h @@ -24,15 +24,15 @@ class MarkdownCommitHook : public UIManagerCommitHook { ~MarkdownCommitHook() noexcept override; - void commitHookWasRegistered(UIManager const &) const noexcept override {} + void commitHookWasRegistered(UIManager const &) noexcept override {} - void commitHookWasUnregistered(UIManager const &) const noexcept override {} + void commitHookWasUnregistered(UIManager const &) noexcept override {} RootShadowNode::Unshared shadowTreeWillCommit( ShadowTree const &shadowTree, RootShadowNode::Shared const &oldRootShadowNode, RootShadowNode::Unshared const &newRootShadowNode) - const noexcept override; + noexcept override; private: std::shared_ptr uiManager_; diff --git a/ios/MarkdownCommitHook.mm b/ios/MarkdownCommitHook.mm index d610b047..f0855c17 100644 --- a/ios/MarkdownCommitHook.mm +++ b/ios/MarkdownCommitHook.mm @@ -25,7 +25,7 @@ RootShadowNode::Unshared MarkdownCommitHook::shadowTreeWillCommit( ShadowTree const &, RootShadowNode::Shared const &, - RootShadowNode::Unshared const &newRootShadowNode) const noexcept { + RootShadowNode::Unshared const &newRootShadowNode) noexcept { auto rootNode = newRootShadowNode->ShadowNode::clone(ShadowNodeFragment{}); // A preface to why we do the weird thing below: diff --git a/ios/MarkdownShadowFamilyRegistry.h b/ios/MarkdownShadowFamilyRegistry.h index df4aa554..6075f3f4 100644 --- a/ios/MarkdownShadowFamilyRegistry.h +++ b/ios/MarkdownShadowFamilyRegistry.h @@ -1,6 +1,7 @@ #include #include +#include // A registry to store pointers to the ShadowNodeFamilies of markdown decorators. // The only place we can _legally_ access the family of shadow node is in the constructor From a3da02b1ffd9124b9ce173fc27916156b5c874a5 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Mon, 26 Feb 2024 17:51:01 +0100 Subject: [PATCH 14/35] Fix layout on reload --- ios/MarkdownCommitHook.mm | 6 ++++- ios/MarkdownShadowFamilyRegistry.cpp | 29 ++++++++++++++++++----- ios/MarkdownShadowFamilyRegistry.h | 7 ++++-- ios/RCTLiveMarkdownModule.mm | 2 +- ios/RCTTextInputComponentView+Markdown.mm | 8 +++++++ 5 files changed, 42 insertions(+), 10 deletions(-) diff --git a/ios/MarkdownCommitHook.mm b/ios/MarkdownCommitHook.mm index f0855c17..5bee8c80 100644 --- a/ios/MarkdownCommitHook.mm +++ b/ios/MarkdownCommitHook.mm @@ -80,7 +80,11 @@ // only source of that pointer (besides this commit hook) is RCTTextInputComponentView, which // has the relevant method swizzled to make sure the markdown styles are always applied before // updating state - if (stateData.attributedStringBox.getMode() == AttributedStringBox::Mode::Value) { + // There is one caveat, on the first render the swizzled method will not apply markdown since + // the native component is not mounted yet. In that case we save the tag to update in the + // method applying markdown formatting and apply it here instead, preventing wrong layout + // on reloads. + if (stateData.attributedStringBox.getMode() == AttributedStringBox::Mode::Value || MarkdownShadowFamilyRegistry::shouldForceUpdate(nodes.textInput->getTag())) { rootNode = rootNode->cloneTree(nodes.textInput->getFamily(), [&nodes, &textInputState, &stateData](const ShadowNode& node) { const auto &markdownProps = *std::static_pointer_cast(nodes.decorator->getProps()); const auto &textInputProps = *std::static_pointer_cast(nodes.textInput->getProps()); diff --git a/ios/MarkdownShadowFamilyRegistry.cpp b/ios/MarkdownShadowFamilyRegistry.cpp index 848f7726..ae435939 100644 --- a/ios/MarkdownShadowFamilyRegistry.cpp +++ b/ios/MarkdownShadowFamilyRegistry.cpp @@ -1,29 +1,46 @@ #include "MarkdownShadowFamilyRegistry.h" std::set MarkdownShadowFamilyRegistry::_familiesToUpdate; -std::mutex MarkdownShadowFamilyRegistry::_familiesMutex; +std::set MarkdownShadowFamilyRegistry::_forcedUpdates; +std::mutex MarkdownShadowFamilyRegistry::_mutex; void MarkdownShadowFamilyRegistry::registerFamilyForUpdates(facebook::react::ShadowNodeFamily::Shared family) { - auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::_familiesMutex); + auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::_mutex); MarkdownShadowFamilyRegistry::_familiesToUpdate.insert(family); } void MarkdownShadowFamilyRegistry::unregisterFamilyForUpdates(facebook::react::ShadowNodeFamily::Shared family) { - auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::_familiesMutex); + auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::_mutex); MarkdownShadowFamilyRegistry::_familiesToUpdate.erase(family); } -void MarkdownShadowFamilyRegistry::clearRegisteredFamilies() { - auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::_familiesMutex); +void MarkdownShadowFamilyRegistry::reset() { + auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::_mutex); MarkdownShadowFamilyRegistry::_familiesToUpdate.clear(); + MarkdownShadowFamilyRegistry::_forcedUpdates.clear(); } void MarkdownShadowFamilyRegistry::runForEveryFamily(std::function fun) { - auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::_familiesMutex); + auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::_mutex); for (auto &family : MarkdownShadowFamilyRegistry::_familiesToUpdate) { fun(family); } } + +void MarkdownShadowFamilyRegistry::forceNextStateUpdate(facebook::react::Tag tag) { + auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::_mutex); + _forcedUpdates.insert(tag); +} + +bool MarkdownShadowFamilyRegistry::shouldForceUpdate(facebook::react::Tag tag) { + auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::_mutex); + bool force = _forcedUpdates.contains(tag); + if (force) { + _forcedUpdates.erase(tag); + return true; + } + return false; +} diff --git a/ios/MarkdownShadowFamilyRegistry.h b/ios/MarkdownShadowFamilyRegistry.h index 6075f3f4..7d81eb15 100644 --- a/ios/MarkdownShadowFamilyRegistry.h +++ b/ios/MarkdownShadowFamilyRegistry.h @@ -13,10 +13,13 @@ class MarkdownShadowFamilyRegistry { public: static void registerFamilyForUpdates(facebook::react::ShadowNodeFamily::Shared family); static void unregisterFamilyForUpdates(facebook::react::ShadowNodeFamily::Shared family); - static void clearRegisteredFamilies(); + static void reset(); static void runForEveryFamily(std::function fun); + static void forceNextStateUpdate(facebook::react::Tag tag); + static bool shouldForceUpdate(facebook::react::Tag tag); private: static std::set _familiesToUpdate; - static std::mutex _familiesMutex; + static std::set _forcedUpdates; + static std::mutex _mutex; }; diff --git a/ios/RCTLiveMarkdownModule.mm b/ios/RCTLiveMarkdownModule.mm index 4af759c5..a579098b 100644 --- a/ios/RCTLiveMarkdownModule.mm +++ b/ios/RCTLiveMarkdownModule.mm @@ -38,7 +38,7 @@ - (NSNumber*)install - (void)invalidate { - MarkdownShadowFamilyRegistry::clearRegisteredFamilies(); + MarkdownShadowFamilyRegistry::reset(); [super invalidate]; } diff --git a/ios/RCTTextInputComponentView+Markdown.mm b/ios/RCTTextInputComponentView+Markdown.mm index 7d05bf4a..7c79cfed 100644 --- a/ios/RCTTextInputComponentView+Markdown.mm +++ b/ios/RCTTextInputComponentView+Markdown.mm @@ -6,6 +6,8 @@ #import #import +#import "MarkdownShadowFamilyRegistry.h" + @implementation RCTTextInputComponentView (Markdown) - (void)setMarkdownUtils:(RCTMarkdownUtils *)markdownUtils { @@ -33,6 +35,12 @@ - (void)markdown__setAttributedString:(NSAttributedString *)attributedString RCTUITextField *backedTextInputView = [self getBackedTextInputView]; if (markdownUtils != nil && backedTextInputView != nil) { attributedString = [markdownUtils parseMarkdown:attributedString withAttributes: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 From 1b93823119732351f998e9e0a65023db45c3b38f Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 27 Feb 2024 09:51:26 +0100 Subject: [PATCH 15/35] Update ios/RCTLiveMarkdownModule.mm Co-authored-by: Tomek Zawadzki --- ios/RCTLiveMarkdownModule.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/RCTLiveMarkdownModule.mm b/ios/RCTLiveMarkdownModule.mm index a579098b..3c488711 100644 --- a/ios/RCTLiveMarkdownModule.mm +++ b/ios/RCTLiveMarkdownModule.mm @@ -17,7 +17,7 @@ @implementation RCTLiveMarkdownModule { RCT_EXPORT_MODULE(@"RNLiveMarkdownModule") -- (NSNumber*)install +- (NSNumber *)install { if (!installed_) { installed_ = YES; From 47de39461cb54498d2e3487198a22cc2281f8037 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 27 Feb 2024 09:51:33 +0100 Subject: [PATCH 16/35] Update ios/MarkdownCommitHook.mm Co-authored-by: Tomek Zawadzki --- ios/MarkdownCommitHook.mm | 1 - 1 file changed, 1 deletion(-) diff --git a/ios/MarkdownCommitHook.mm b/ios/MarkdownCommitHook.mm index 5bee8c80..58aa1c99 100644 --- a/ios/MarkdownCommitHook.mm +++ b/ios/MarkdownCommitHook.mm @@ -1,4 +1,3 @@ - #ifdef RCT_NEW_ARCH_ENABLED #include From d79c99219ca132ed58a5dd532e5e5680edb773b3 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 27 Feb 2024 09:51:41 +0100 Subject: [PATCH 17/35] Update ios/MarkdownTextInputDecoratorState.h Co-authored-by: Tomek Zawadzki --- ios/MarkdownTextInputDecoratorState.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/ios/MarkdownTextInputDecoratorState.h b/ios/MarkdownTextInputDecoratorState.h index 144ca580..67c77a8b 100644 --- a/ios/MarkdownTextInputDecoratorState.h +++ b/ios/MarkdownTextInputDecoratorState.h @@ -13,8 +13,6 @@ class JSI_EXPORT MarkdownTextInputDecoratorState final { MarkdownTextInputDecoratorState(const ShadowNodeFamily::Shared textInputFamily_) : decoratorFamily(textInputFamily_) {}; const ShadowNodeFamily::Shared decoratorFamily; - -#pragma mark - Getters }; } // namespace react From 33b3ab697d6faf45cca15b0edef0b0cd08e93917 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 27 Feb 2024 09:51:49 +0100 Subject: [PATCH 18/35] Update ios/MarkdownCommitHook.h Co-authored-by: Tomek Zawadzki --- ios/MarkdownCommitHook.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/MarkdownCommitHook.h b/ios/MarkdownCommitHook.h index 450f02c5..96b52a64 100644 --- a/ios/MarkdownCommitHook.h +++ b/ios/MarkdownCommitHook.h @@ -14,8 +14,8 @@ using namespace facebook::react; namespace livemarkdown { struct MarkdownTextInputDecoratorPair { - std::shared_ptr textInput; - std::shared_ptr decorator; + const std::shared_ptr textInput; + const std::shared_ptr decorator; }; class MarkdownCommitHook : public UIManagerCommitHook { From 34b5062130bad1c4d68282c1e0a4ef3784002761 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 27 Feb 2024 09:52:09 +0100 Subject: [PATCH 19/35] Update ios/MarkdownCommitHook.mm Co-authored-by: Tomek Zawadzki --- ios/MarkdownCommitHook.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/MarkdownCommitHook.mm b/ios/MarkdownCommitHook.mm index 58aa1c99..3d9f140f 100644 --- a/ios/MarkdownCommitHook.mm +++ b/ios/MarkdownCommitHook.mm @@ -70,7 +70,7 @@ } }); - for (auto &nodes : nodesToUpdate) { + for (const auto &nodes : nodesToUpdate) { const auto &textInputState = *std::static_pointer_cast>(nodes.textInput->getState()); const auto &stateData = textInputState.getData(); From 916c5d600d97ca55df30d279e1b0cd45862f0bc0 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 27 Feb 2024 09:52:27 +0100 Subject: [PATCH 20/35] Update ios/MarkdownTextInputDecoratorView.mm Co-authored-by: Tomek Zawadzki --- ios/MarkdownTextInputDecoratorView.mm | 3 --- 1 file changed, 3 deletions(-) diff --git a/ios/MarkdownTextInputDecoratorView.mm b/ios/MarkdownTextInputDecoratorView.mm index 4ca1906f..9edae183 100644 --- a/ios/MarkdownTextInputDecoratorView.mm +++ b/ios/MarkdownTextInputDecoratorView.mm @@ -63,9 +63,6 @@ - (void)didMoveToWindow { [_markdownUtils setMarkdownStyle:_markdownStyle]; [_textInput setMarkdownUtils:_markdownUtils]; - -// [_textInput performSelector:@selector(_updateState)]; - if ([backedTextInputView isKindOfClass:[RCTUITextField class]]) { RCTUITextField *textField = (RCTUITextField *)backedTextInputView; _adapter = [textField valueForKey:@"textInputDelegateAdapter"]; From e2701ff1e0062b970753b5e2db4b9c9642a40a74 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 27 Feb 2024 09:54:03 +0100 Subject: [PATCH 21/35] Update ios/MarkdownCommitHook.h Co-authored-by: Tomek Zawadzki --- ios/MarkdownCommitHook.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/MarkdownCommitHook.h b/ios/MarkdownCommitHook.h index 96b52a64..58804955 100644 --- a/ios/MarkdownCommitHook.h +++ b/ios/MarkdownCommitHook.h @@ -35,7 +35,7 @@ class MarkdownCommitHook : public UIManagerCommitHook { noexcept override; private: - std::shared_ptr uiManager_; + const std::shared_ptr uiManager_; }; } // namespace reanimated From 3e6e2575b1e4ea8e324e84b7745ac9e6889e8fce Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 27 Feb 2024 09:55:14 +0100 Subject: [PATCH 22/35] Update ios/MarkdownCommitHook.mm Co-authored-by: Tomek Zawadzki --- ios/MarkdownCommitHook.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/MarkdownCommitHook.mm b/ios/MarkdownCommitHook.mm index 3d9f140f..a45b9bc2 100644 --- a/ios/MarkdownCommitHook.mm +++ b/ios/MarkdownCommitHook.mm @@ -49,7 +49,7 @@ std::vector nodesToUpdate; MarkdownShadowFamilyRegistry::runForEveryFamily([&rootNode, &nodesToUpdate](ShadowNodeFamily::Shared family) { // get the path from the root to the node from the decorator family - auto ancestors = family->getAncestors(*rootNode); + const auto ancestors = family->getAncestors(*rootNode); if (!ancestors.empty()) { auto &parentNode = ancestors.back().first.get(); From 54dcad2366bc4743322d666e8a1b63bae893bfca Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 27 Feb 2024 09:56:55 +0100 Subject: [PATCH 23/35] Sort imports --- ios/MarkdownCommitHook.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/MarkdownCommitHook.mm b/ios/MarkdownCommitHook.mm index 5bee8c80..42c780ee 100644 --- a/ios/MarkdownCommitHook.mm +++ b/ios/MarkdownCommitHook.mm @@ -6,9 +6,9 @@ #include #include "MarkdownCommitHook.h" +#include "MarkdownShadowFamilyRegistry.h" #include "RCTMarkdownStyle.h" #include "RCTMarkdownUtils.h" -#include "MarkdownShadowFamilyRegistry.h" using namespace facebook::react; From 48b0b27355437a9c52779fe1c5f9466873a3f703 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 27 Feb 2024 09:57:12 +0100 Subject: [PATCH 24/35] Fix indent --- ios/MarkdownCommitHook.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/MarkdownCommitHook.h b/ios/MarkdownCommitHook.h index 450f02c5..3f986103 100644 --- a/ios/MarkdownCommitHook.h +++ b/ios/MarkdownCommitHook.h @@ -20,7 +20,7 @@ struct MarkdownTextInputDecoratorPair { class MarkdownCommitHook : public UIManagerCommitHook { public: - MarkdownCommitHook(const std::shared_ptr &uiManager); + MarkdownCommitHook(const std::shared_ptr &uiManager); ~MarkdownCommitHook() noexcept override; From 4e4f30541cadea53a0a4c4a4e7c820c4c2d7a09c Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 27 Feb 2024 10:06:00 +0100 Subject: [PATCH 25/35] Move cast, add some const refs --- ios/MarkdownCommitHook.mm | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ios/MarkdownCommitHook.mm b/ios/MarkdownCommitHook.mm index 42c780ee..b2c8ace5 100644 --- a/ios/MarkdownCommitHook.mm +++ b/ios/MarkdownCommitHook.mm @@ -56,16 +56,19 @@ auto &parentNode = ancestors.back().first.get(); auto index = ancestors.back().second; - auto markdownNode = parentNode.getChildren().at(index); + // this is node represented by one of the registered families and since we only register markdown decorator + // shadow families, static casting should be safe here + const auto& decoratorNode = + std::static_pointer_cast(parentNode.getChildren().at(index)); // text input always precedes the decorator component - auto previousSibling = parentNode.getChildren().at(index - 1); + const auto& previousSibling = parentNode.getChildren().at(index - 1); - if (auto textInputNode = std::dynamic_pointer_cast(previousSibling)) { + if (const auto& textInputNode = std::dynamic_pointer_cast(previousSibling)) { // store the pair of text input and decorator to update in the next step // we need both, decorator to get markdown style and text input to update it nodesToUpdate.push_back({ textInputNode, - std::dynamic_pointer_cast(markdownNode), + decoratorNode, }); } } From ae0d8ba6ce35c60008d131c224eca6783e673e23 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 27 Feb 2024 10:07:39 +0100 Subject: [PATCH 26/35] Rename fields --- ios/MarkdownShadowFamilyRegistry.cpp | 34 ++++++++++++++-------------- ios/MarkdownShadowFamilyRegistry.h | 6 ++--- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/ios/MarkdownShadowFamilyRegistry.cpp b/ios/MarkdownShadowFamilyRegistry.cpp index ae435939..348c7878 100644 --- a/ios/MarkdownShadowFamilyRegistry.cpp +++ b/ios/MarkdownShadowFamilyRegistry.cpp @@ -1,45 +1,45 @@ #include "MarkdownShadowFamilyRegistry.h" -std::set MarkdownShadowFamilyRegistry::_familiesToUpdate; -std::set MarkdownShadowFamilyRegistry::_forcedUpdates; -std::mutex MarkdownShadowFamilyRegistry::_mutex; +std::set MarkdownShadowFamilyRegistry::familiesToUpdate_; +std::set MarkdownShadowFamilyRegistry::forcedUpdates_; +std::mutex MarkdownShadowFamilyRegistry::mutex_; void MarkdownShadowFamilyRegistry::registerFamilyForUpdates(facebook::react::ShadowNodeFamily::Shared family) { - auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::_mutex); - MarkdownShadowFamilyRegistry::_familiesToUpdate.insert(family); + auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::mutex_); + MarkdownShadowFamilyRegistry::familiesToUpdate_.insert(family); } void MarkdownShadowFamilyRegistry::unregisterFamilyForUpdates(facebook::react::ShadowNodeFamily::Shared family) { - auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::_mutex); - MarkdownShadowFamilyRegistry::_familiesToUpdate.erase(family); + auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::mutex_); + MarkdownShadowFamilyRegistry::familiesToUpdate_.erase(family); } void MarkdownShadowFamilyRegistry::reset() { - auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::_mutex); - MarkdownShadowFamilyRegistry::_familiesToUpdate.clear(); - MarkdownShadowFamilyRegistry::_forcedUpdates.clear(); + auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::mutex_); + MarkdownShadowFamilyRegistry::familiesToUpdate_.clear(); + MarkdownShadowFamilyRegistry::forcedUpdates_.clear(); } void MarkdownShadowFamilyRegistry::runForEveryFamily(std::function fun) { - auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::_mutex); - for (auto &family : MarkdownShadowFamilyRegistry::_familiesToUpdate) { + auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::mutex_); + for (auto &family : MarkdownShadowFamilyRegistry::familiesToUpdate_) { fun(family); } } void MarkdownShadowFamilyRegistry::forceNextStateUpdate(facebook::react::Tag tag) { - auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::_mutex); - _forcedUpdates.insert(tag); + auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::mutex_); + forcedUpdates_.insert(tag); } bool MarkdownShadowFamilyRegistry::shouldForceUpdate(facebook::react::Tag tag) { - auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::_mutex); - bool force = _forcedUpdates.contains(tag); + auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::mutex_); + bool force = forcedUpdates_.contains(tag); if (force) { - _forcedUpdates.erase(tag); + forcedUpdates_.erase(tag); return true; } return false; diff --git a/ios/MarkdownShadowFamilyRegistry.h b/ios/MarkdownShadowFamilyRegistry.h index 7d81eb15..42ee320f 100644 --- a/ios/MarkdownShadowFamilyRegistry.h +++ b/ios/MarkdownShadowFamilyRegistry.h @@ -19,7 +19,7 @@ class MarkdownShadowFamilyRegistry { static bool shouldForceUpdate(facebook::react::Tag tag); private: - static std::set _familiesToUpdate; - static std::set _forcedUpdates; - static std::mutex _mutex; + static std::set familiesToUpdate_; + static std::set forcedUpdates_; + static std::mutex mutex_; }; From 2a423ff4e516adc75be9ea939fc6fc730169446a Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 27 Feb 2024 10:13:09 +0100 Subject: [PATCH 27/35] Rename module, drop prefix --- ios/RCTLiveMarkdownModule.h | 2 +- ios/RCTLiveMarkdownModule.mm | 4 ++-- src/MarkdownTextInput.tsx | 6 +++--- ...{NativeMarkdownModule.ts => NativeLiveMarkdownModule.ts} | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) rename src/{NativeMarkdownModule.ts => NativeLiveMarkdownModule.ts} (69%) diff --git a/ios/RCTLiveMarkdownModule.h b/ios/RCTLiveMarkdownModule.h index b175a0b2..99cfdc0b 100644 --- a/ios/RCTLiveMarkdownModule.h +++ b/ios/RCTLiveMarkdownModule.h @@ -5,7 +5,7 @@ #import // Without inheriting after RCTEventEmitter we don't get access to bridge -@interface RCTLiveMarkdownModule : RCTEventEmitter +@interface RCTLiveMarkdownModule : RCTEventEmitter @end #endif // RCT_NEW_ARCH_ENABLED diff --git a/ios/RCTLiveMarkdownModule.mm b/ios/RCTLiveMarkdownModule.mm index a579098b..a2e93802 100644 --- a/ios/RCTLiveMarkdownModule.mm +++ b/ios/RCTLiveMarkdownModule.mm @@ -15,7 +15,7 @@ @implementation RCTLiveMarkdownModule { std::shared_ptr commitHook_; } -RCT_EXPORT_MODULE(@"RNLiveMarkdownModule") +RCT_EXPORT_MODULE(@"LiveMarkdownModule") - (NSNumber*)install { @@ -33,7 +33,7 @@ - (NSNumber*)install - (std::shared_ptr)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params { - return std::make_shared(params); + return std::make_shared(params); } - (void)invalidate diff --git a/src/MarkdownTextInput.tsx b/src/MarkdownTextInput.tsx index bafdea18..95a24f0a 100644 --- a/src/MarkdownTextInput.tsx +++ b/src/MarkdownTextInput.tsx @@ -2,13 +2,13 @@ import {StyleSheet, TextInput, processColor} from 'react-native'; import React from 'react'; import type {TextInputProps} from 'react-native'; import MarkdownTextInputDecoratorViewNativeComponent from './MarkdownTextInputDecoratorViewNativeComponent'; -import NativeMarkdownModule from './NativeMarkdownModule'; +import NativeLiveMarkdownModule from './NativeLiveMarkdownModule'; import type * as MarkdownTextInputDecoratorViewNativeComponentTypes from './MarkdownTextInputDecoratorViewNativeComponent'; import * as StyleUtils from './styleUtils'; import type * as StyleUtilsTypes from './styleUtils'; -if (NativeMarkdownModule) { - NativeMarkdownModule.install(); +if (NativeLiveMarkdownModule) { + NativeLiveMarkdownModule.install(); } type PartialMarkdownStyle = StyleUtilsTypes.PartialMarkdownStyle; diff --git a/src/NativeMarkdownModule.ts b/src/NativeLiveMarkdownModule.ts similarity index 69% rename from src/NativeMarkdownModule.ts rename to src/NativeLiveMarkdownModule.ts index 6a6c2649..c847838f 100644 --- a/src/NativeMarkdownModule.ts +++ b/src/NativeLiveMarkdownModule.ts @@ -5,4 +5,4 @@ interface Spec extends TurboModule { install: () => boolean; } -export default TurboModuleRegistry.get('RCTLiveMarkdownModule'); +export default TurboModuleRegistry.get('LiveMarkdownModule'); From 8fcaa4ecdfa1c35b53cb76532c84b92e4ccbaf90 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Thu, 28 Mar 2024 10:35:16 +0100 Subject: [PATCH 28/35] Dont use UI APIs on js thread --- ios/MarkdownCommitHook.h | 2 +- ios/MarkdownCommitHook.mm | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ios/MarkdownCommitHook.h b/ios/MarkdownCommitHook.h index feb25716..eb31e9d3 100644 --- a/ios/MarkdownCommitHook.h +++ b/ios/MarkdownCommitHook.h @@ -38,6 +38,6 @@ class MarkdownCommitHook : public UIManagerCommitHook { const std::shared_ptr uiManager_; }; -} // namespace reanimated +} // namespace livemarkdown #endif // RCT_NEW_ARCH_ENABLED diff --git a/ios/MarkdownCommitHook.mm b/ios/MarkdownCommitHook.mm index 83449aff..f9526322 100644 --- a/ios/MarkdownCommitHook.mm +++ b/ios/MarkdownCommitHook.mm @@ -76,6 +76,7 @@ for (const auto &nodes : nodesToUpdate) { const auto &textInputState = *std::static_pointer_cast>(nodes.textInput->getState()); const auto &stateData = textInputState.getData(); + const auto fontSizeMultiplier = newRootShadowNode->getConcreteProps().layoutContext.fontSizeMultiplier; // We only want to update the shadow node when the attributed string is stored by value // If it's stored by pointer, the markdown formatting should already by applied to it, since the @@ -87,11 +88,11 @@ // method applying markdown formatting and apply it here instead, preventing wrong layout // on reloads. if (stateData.attributedStringBox.getMode() == AttributedStringBox::Mode::Value || MarkdownShadowFamilyRegistry::shouldForceUpdate(nodes.textInput->getTag())) { - rootNode = rootNode->cloneTree(nodes.textInput->getFamily(), [&nodes, &textInputState, &stateData](const ShadowNode& node) { + rootNode = rootNode->cloneTree(nodes.textInput->getFamily(), [&nodes, &textInputState, &stateData, fontSizeMultiplier](const ShadowNode& node) { const auto &markdownProps = *std::static_pointer_cast(nodes.decorator->getProps()); const auto &textInputProps = *std::static_pointer_cast(nodes.textInput->getProps()); - const auto defaultTextAttributes = textInputProps.getEffectiveTextAttributes(RCTFontSizeMultiplier()); + const auto defaultTextAttributes = textInputProps.getEffectiveTextAttributes(fontSizeMultiplier); const auto defaultNSTextAttributes = RCTNSTextAttributesFromTextAttributes(defaultTextAttributes); // this can possibly be optimized From 17e4c1ecddc9c2398d4e01d418b287c4f029f8ce Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Mon, 8 Apr 2024 12:20:13 +0200 Subject: [PATCH 29/35] Update styles on props change --- ios/MarkdownCommitHook.mm | 12 +++++++----- ios/MarkdownShadowFamilyRegistry.h | 2 ++ ios/MarkdownTextInputDecoratorShadowNode.h | 8 +++++++- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/ios/MarkdownCommitHook.mm b/ios/MarkdownCommitHook.mm index f9526322..26bb84cc 100644 --- a/ios/MarkdownCommitHook.mm +++ b/ios/MarkdownCommitHook.mm @@ -82,11 +82,13 @@ // If it's stored by pointer, the markdown formatting should already by applied to it, since the // only source of that pointer (besides this commit hook) is RCTTextInputComponentView, which // has the relevant method swizzled to make sure the markdown styles are always applied before - // updating state - // There is one caveat, on the first render the swizzled method will not apply markdown since - // the native component is not mounted yet. In that case we save the tag to update in the - // method applying markdown formatting and apply it here instead, preventing wrong layout - // on reloads. + // updating state. + // There are two caveats: + // 1. On the first render the swizzled method will not apply markdown since the native component + // is not mounted yet. In that case we save the tag to update in the method applying markdown + // formatting and apply it here instead, preventing wrong layout on reloads. + // 2. When the markdown style prop is changed, the native state needs to be updated to reflect + // them. In that case the relevant tag is saved in the registry when the new shadow node is created. if (stateData.attributedStringBox.getMode() == AttributedStringBox::Mode::Value || MarkdownShadowFamilyRegistry::shouldForceUpdate(nodes.textInput->getTag())) { rootNode = rootNode->cloneTree(nodes.textInput->getFamily(), [&nodes, &textInputState, &stateData, fontSizeMultiplier](const ShadowNode& node) { const auto &markdownProps = *std::static_pointer_cast(nodes.decorator->getProps()); diff --git a/ios/MarkdownShadowFamilyRegistry.h b/ios/MarkdownShadowFamilyRegistry.h index 42ee320f..e0da39e1 100644 --- a/ios/MarkdownShadowFamilyRegistry.h +++ b/ios/MarkdownShadowFamilyRegistry.h @@ -1,3 +1,5 @@ +#pragma once + #include #include diff --git a/ios/MarkdownTextInputDecoratorShadowNode.h b/ios/MarkdownTextInputDecoratorShadowNode.h index cd3aea68..3045a36f 100644 --- a/ios/MarkdownTextInputDecoratorShadowNode.h +++ b/ios/MarkdownTextInputDecoratorShadowNode.h @@ -1,6 +1,7 @@ #pragma once #include "MarkdownTextInputDecoratorState.h" +#include "MarkdownShadowFamilyRegistry.h" #include #include #include @@ -28,7 +29,12 @@ class JSI_EXPORT MarkdownTextInputDecoratorShadowNode final : public ConcreteVie MarkdownTextInputDecoratorShadowNode( ShadowNode const &sourceShadowNode, ShadowNodeFragment const &fragment) - : ConcreteViewShadowNode(sourceShadowNode, fragment) {} + : ConcreteViewShadowNode(sourceShadowNode, fragment) { + // if the props changed, we need to update the shadow node state to reflect potential style changes + if (fragment.props != ShadowNodeFragment::propsPlaceholder()) { + MarkdownShadowFamilyRegistry::forceNextStateUpdate(this->getTag()); + } + } private: static const ShadowNodeFragment::Value updateFragmentState(ShadowNodeFragment const &fragment, ShadowNodeFamily::Shared const &family); From dcab103ed812044601d962ab59283161f62e3fbd Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 9 Apr 2024 09:19:53 +0200 Subject: [PATCH 30/35] Revert new-arch flags and deps --- .../project.pbxproj | 8 +- example/ios/Podfile.lock | 104 +++--------------- 2 files changed, 22 insertions(+), 90 deletions(-) diff --git a/example/ios/LiveMarkdownExample.xcodeproj/project.pbxproj b/example/ios/LiveMarkdownExample.xcodeproj/project.pbxproj index 92e4143e..2bfea67a 100644 --- a/example/ios/LiveMarkdownExample.xcodeproj/project.pbxproj +++ b/example/ios/LiveMarkdownExample.xcodeproj/project.pbxproj @@ -574,7 +574,7 @@ ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "$(inherited)", - "-DRN_FABRIC_ENABLED", + " ", ); OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", @@ -582,7 +582,7 @@ "-DFOLLY_MOBILE=1", "-DFOLLY_USE_LIBCPP=1", "-DFOLLY_CFG_NO_COROUTINES=1", - "-DRN_FABRIC_ENABLED", + " ", ); OTHER_LDFLAGS = ( "$(inherited)", @@ -649,7 +649,7 @@ MTL_ENABLE_DEBUG_INFO = NO; OTHER_CFLAGS = ( "$(inherited)", - "-DRN_FABRIC_ENABLED", + " ", ); OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", @@ -657,7 +657,7 @@ "-DFOLLY_MOBILE=1", "-DFOLLY_USE_LIBCPP=1", "-DFOLLY_CFG_NO_COROUTINES=1", - "-DRN_FABRIC_ENABLED", + " ", ); OTHER_LDFLAGS = ( "$(inherited)", diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 9f49431e..b14effe1 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -3,6 +3,13 @@ PODS: - CocoaAsyncSocket (7.6.5) - DoubleConversion (1.1.6) - FBLazyVector (0.73.4) + - FBReactNativeSpec (0.73.4): + - RCT-Folly (= 2022.05.16.00) + - RCTRequired (= 0.73.4) + - RCTTypeSafety (= 0.73.4) + - React-Core (= 0.73.4) + - React-jsi (= 0.73.4) + - ReactCommon/turbomodule/core (= 0.73.4) - Flipper (0.201.0): - Flipper-Folly (~> 2.6) - Flipper-Boost-iOSX (1.76.0.1.11) @@ -109,21 +116,17 @@ PODS: - React-callinvoker (0.73.4) - React-Codegen (0.73.4): - DoubleConversion + - FBReactNativeSpec - glog - hermes-engine - RCT-Folly - RCTRequired - RCTTypeSafety - React-Core - - React-debug - - React-Fabric - - React-FabricImage - - React-graphics - React-jsi - React-jsiexecutor - React-NativeModulesApple - - React-rendererdebug - - React-utils + - React-rncore - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - React-Core (0.73.4): @@ -936,8 +939,6 @@ PODS: - React-jsi (= 0.73.4) - React-perflogger (= 0.73.4) - React-jsinspector (0.73.4) - - React-jsitracing (0.73.4): - - React-jsi - React-logger (0.73.4): - glog - React-Mapbuffer (0.73.4): @@ -971,21 +972,13 @@ PODS: - RCTTypeSafety - React-Core - React-CoreModules - - React-debug - - React-Fabric - - React-graphics - React-hermes - React-nativeconfig - React-NativeModulesApple - React-RCTFabric - React-RCTImage - React-RCTNetwork - - React-rendererdebug - - React-RuntimeApple - - React-RuntimeCore - - React-RuntimeHermes - React-runtimescheduler - - React-utils - ReactCommon - React-RCTBlob (0.73.4): - hermes-engine @@ -1063,42 +1056,8 @@ PODS: - RCT-Folly (= 2022.05.16.00) - React-debug - React-rncore (0.73.4) - - React-RuntimeApple (0.73.4): - - hermes-engine - - RCT-Folly/Fabric (= 2022.05.16.00) - - React-callinvoker - - React-Core/Default - - React-CoreModules - - React-cxxreact - - React-jserrorhandler - - React-jsi - - React-jsiexecutor - - React-Mapbuffer - - React-NativeModulesApple - - React-RCTFabric - - React-RuntimeCore - - React-runtimeexecutor - - React-RuntimeHermes - - React-utils - - React-RuntimeCore (0.73.4): - - glog - - hermes-engine - - RCT-Folly/Fabric (= 2022.05.16.00) - - React-cxxreact - - React-jserrorhandler - - React-jsi - - React-jsiexecutor - - React-runtimeexecutor - - React-runtimescheduler - React-runtimeexecutor (0.73.4): - React-jsi (= 0.73.4) - - React-RuntimeHermes (0.73.4): - - hermes-engine - - RCT-Folly/Fabric (= 2022.05.16.00) - - React-jsi - - React-jsitracing - - React-nativeconfig - - React-utils - React-runtimescheduler (0.73.4): - glog - hermes-engine @@ -1152,25 +1111,10 @@ PODS: - React-jsi (= 0.73.4) - React-logger (= 0.73.4) - React-perflogger (= 0.73.4) - - RNLiveMarkdown (0.1.33): + - RNLiveMarkdown (0.1.38): - glog - - hermes-engine - RCT-Folly (= 2022.05.16.00) - - RCTRequired - - RCTTypeSafety - - React-Codegen - React-Core - - React-debug - - React-Fabric - - React-graphics - - React-ImageManager - - React-NativeModulesApple - - React-RCTFabric - - React-rendererdebug - - React-utils - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - Yoga - SocketRocket (0.6.1) - Yoga (1.14.0) @@ -1178,6 +1122,7 @@ DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) + - FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`) - Flipper (= 0.201.0) - Flipper-Boost-iOSX (= 1.76.0.1.11) - Flipper-DoubleConversion (= 3.2.0.1) @@ -1224,7 +1169,6 @@ DEPENDENCIES: - React-jsi (from `../node_modules/react-native/ReactCommon/jsi`) - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector-modern`) - - React-jsitracing (from `../node_modules/react-native/ReactCommon/hermes/executor/`) - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) - React-nativeconfig (from `../node_modules/react-native/ReactCommon`) @@ -1243,10 +1187,7 @@ DEPENDENCIES: - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) - React-rendererdebug (from `../node_modules/react-native/ReactCommon/react/renderer/debug`) - React-rncore (from `../node_modules/react-native/ReactCommon`) - - React-RuntimeApple (from `../node_modules/react-native/ReactCommon/react/runtime/platform/ios`) - - React-RuntimeCore (from `../node_modules/react-native/ReactCommon/react/runtime`) - React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`) - - React-RuntimeHermes (from `../node_modules/react-native/ReactCommon/react/runtime`) - React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) @@ -1276,6 +1217,8 @@ EXTERNAL SOURCES: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" FBLazyVector: :path: "../node_modules/react-native/Libraries/FBLazyVector" + FBReactNativeSpec: + :path: "../node_modules/react-native/React/FBReactNativeSpec" glog: :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" hermes-engine: @@ -1319,8 +1262,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/jsiexecutor" React-jsinspector: :path: "../node_modules/react-native/ReactCommon/jsinspector-modern" - React-jsitracing: - :path: "../node_modules/react-native/ReactCommon/hermes/executor/" React-logger: :path: "../node_modules/react-native/ReactCommon/logger" React-Mapbuffer: @@ -1357,14 +1298,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/react/renderer/debug" React-rncore: :path: "../node_modules/react-native/ReactCommon" - React-RuntimeApple: - :path: "../node_modules/react-native/ReactCommon/react/runtime/platform/ios" - React-RuntimeCore: - :path: "../node_modules/react-native/ReactCommon/react/runtime" React-runtimeexecutor: :path: "../node_modules/react-native/ReactCommon/runtimeexecutor" - React-RuntimeHermes: - :path: "../node_modules/react-native/ReactCommon/react/runtime" React-runtimescheduler: :path: "../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler" React-utils: @@ -1381,6 +1316,7 @@ SPEC CHECKSUMS: CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: fea03f2699887d960129cc54bba7e52542b6f953 FBLazyVector: 84f6edbe225f38aebd9deaf1540a4160b1f087d7 + FBReactNativeSpec: d0086a479be91c44ce4687a962956a352d2dc697 Flipper: c7a0093234c4bdd456e363f2f19b2e4b27652d44 Flipper-Boost-iOSX: fd1e2b8cbef7e662a122412d7ac5f5bea715403c Flipper-DoubleConversion: 2dc99b02f658daf147069aad9dbd29d8feb06d30 @@ -1399,7 +1335,7 @@ SPEC CHECKSUMS: RCTTypeSafety: 63b97ced7b766865057e7154db0e81ce4ee6cf1e React: 1c87497e50fa40ba9c54e5ea5e53483a0f8eecc0 React-callinvoker: e3a52a9a93e3eb004d7282c26a4fb27003273fe6 - React-Codegen: 05f85e0e087f402978cfe57822a8debc07127a13 + React-Codegen: 50c0f8f073e71b929b057b68bf31be604f1dccc8 React-Core: d0ecde72894b792cb8922efaa0990199cbe85169 React-CoreModules: 2ff1684dd517f0c441495d90a704d499f05e9d0a React-cxxreact: d9be2fac926741052395da0a6d0bab8d71e2f297 @@ -1413,7 +1349,6 @@ SPEC CHECKSUMS: React-jsi: 380cd24dd81a705dd042c18989fb10b07182210c React-jsiexecutor: 8ed7a18b9f119440efdcd424c8257dc7e18067e2 React-jsinspector: 9ac353eccf6ab54d1e0a33862ba91221d1e88460 - React-jsitracing: e8a2dafb9878dbcad02b6b2b88e66267fb427b74 React-logger: 0a57b68dd2aec7ff738195f081f0520724b35dab React-Mapbuffer: 63913773ed7f96b814a2521e13e6d010282096ad React-nativeconfig: d7af5bae6da70fa15ce44f045621cf99ed24087c @@ -1421,7 +1356,7 @@ SPEC CHECKSUMS: React-perflogger: 8a1e1af5733004bdd91258dcefbde21e0d1faccd React-RCTActionSheet: 64bbff3a3963664c2d0146f870fe8e0264aee4c4 React-RCTAnimation: b698168a7269265a4694727196484342d695f0c1 - React-RCTAppDelegate: a84b4bf99b871d87b41d7a768e7860d207fb4e31 + React-RCTAppDelegate: dcd8e955116eb1d1908dfaf08b4c970812e6a1e6 React-RCTBlob: 47f8c3b2b4b7fa2c5f19c43f0b7f77f57fb9d953 React-RCTFabric: 6067a32d683d0c2b84d444548bc15a263c64abed React-RCTImage: ac0e77a44c290b20db783649b2b9cddc93e3eb99 @@ -1431,15 +1366,12 @@ SPEC CHECKSUMS: React-RCTText: f0176f5f5952f9a4a2c7354f5ae71f7c420aaf34 React-RCTVibration: 8160223c6eda5b187079fec204f80eca8b8f3177 React-rendererdebug: ed286b4da8648c27d6ed3ae1410d4b21ba890d5a - React-rncore: 75cbcc46868e809bb7e738d4565ba85f3dbd5cdc - React-RuntimeApple: f4848a388e4c782d3b8d4ca9c48179163418fe56 - React-RuntimeCore: 272998adc56066404df36b1a3a2be9a56c6ee086 + React-rncore: 43f133b89ac10c4b6ab43702a541dee1c292a3bf React-runtimeexecutor: e6ab6bb083dbdbdd489cff426ed0bce0652e6edf - React-RuntimeHermes: d2c368065ef82d4f8e6daa662352de5a195bdb57 React-runtimescheduler: ed48e5faac6751e66ee1261c4bd01643b436f112 React-utils: 6e5ad394416482ae21831050928ae27348f83487 ReactCommon: 840a955d37b7f3358554d819446bffcf624b2522 - RNLiveMarkdown: 44d4eebd9a4fa032fd6c070ea3c0b7be20ed0802 + RNLiveMarkdown: 64be4c6bbbb6d23bdeece365300ed954486c062d SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70 From 372e41e268f5e568c50bb847feface2b1c7bb6ca Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 9 Apr 2024 09:21:08 +0200 Subject: [PATCH 31/35] Exclude new-arch code on the old-arch --- ios/MarkdownShadowFamilyRegistry.cpp | 4 ++++ ios/MarkdownShadowFamilyRegistry.h | 3 +++ ios/MarkdownTextInputDecoratorShadowNode.cpp | 4 ++++ ios/MarkdownTextInputDecoratorShadowNode.h | 3 +++ 4 files changed, 14 insertions(+) diff --git a/ios/MarkdownShadowFamilyRegistry.cpp b/ios/MarkdownShadowFamilyRegistry.cpp index 348c7878..792b31ff 100644 --- a/ios/MarkdownShadowFamilyRegistry.cpp +++ b/ios/MarkdownShadowFamilyRegistry.cpp @@ -1,3 +1,5 @@ +#ifdef RCT_NEW_ARCH_ENABLED + #include "MarkdownShadowFamilyRegistry.h" std::set MarkdownShadowFamilyRegistry::familiesToUpdate_; @@ -44,3 +46,5 @@ bool MarkdownShadowFamilyRegistry::shouldForceUpdate(facebook::react::Tag tag) { } return false; } + +#endif diff --git a/ios/MarkdownShadowFamilyRegistry.h b/ios/MarkdownShadowFamilyRegistry.h index e0da39e1..49c10688 100644 --- a/ios/MarkdownShadowFamilyRegistry.h +++ b/ios/MarkdownShadowFamilyRegistry.h @@ -1,4 +1,5 @@ #pragma once +#ifdef RCT_NEW_ARCH_ENABLED #include @@ -25,3 +26,5 @@ class MarkdownShadowFamilyRegistry { static std::set forcedUpdates_; static std::mutex mutex_; }; + +#endif diff --git a/ios/MarkdownTextInputDecoratorShadowNode.cpp b/ios/MarkdownTextInputDecoratorShadowNode.cpp index 1e94bc76..ceaa76fb 100644 --- a/ios/MarkdownTextInputDecoratorShadowNode.cpp +++ b/ios/MarkdownTextInputDecoratorShadowNode.cpp @@ -1,3 +1,5 @@ +#ifdef RCT_NEW_ARCH_ENABLED + #include #include "MarkdownTextInputDecoratorShadowNode.h" @@ -25,3 +27,5 @@ const ShadowNodeFragment::Value MarkdownTextInputDecoratorShadowNode::updateFrag } // namespace react } // namespace facebook + +#endif diff --git a/ios/MarkdownTextInputDecoratorShadowNode.h b/ios/MarkdownTextInputDecoratorShadowNode.h index 3045a36f..7187cdc4 100644 --- a/ios/MarkdownTextInputDecoratorShadowNode.h +++ b/ios/MarkdownTextInputDecoratorShadowNode.h @@ -1,4 +1,5 @@ #pragma once +#ifdef RCT_NEW_ARCH_ENABLED #include "MarkdownTextInputDecoratorState.h" #include "MarkdownShadowFamilyRegistry.h" @@ -42,3 +43,5 @@ class JSI_EXPORT MarkdownTextInputDecoratorShadowNode final : public ConcreteVie } // namespace react } // namespace facebook + +#endif From 7bf1f3c6003c3f43d2515761a34eb9190cadb9c5 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 9 Apr 2024 09:43:29 +0200 Subject: [PATCH 32/35] Format code --- ios/MarkdownCommitHook.h | 15 +- ios/MarkdownCommitHook.mm | 280 ++++++++++-------- ios/MarkdownShadowFamilyRegistry.cpp | 64 ++-- ios/MarkdownShadowFamilyRegistry.h | 33 ++- ...ownTextInputDecoratorComponentDescriptor.h | 5 +- ios/MarkdownTextInputDecoratorShadowNode.cpp | 39 ++- ios/MarkdownTextInputDecoratorShadowNode.h | 58 ++-- ios/MarkdownTextInputDecoratorState.h | 8 +- ios/RCTLiveMarkdownModule.h | 8 +- ios/RCTLiveMarkdownModule.mm | 50 ++-- 10 files changed, 315 insertions(+), 245 deletions(-) diff --git a/ios/MarkdownCommitHook.h b/ios/MarkdownCommitHook.h index eb31e9d3..92f0acaf 100644 --- a/ios/MarkdownCommitHook.h +++ b/ios/MarkdownCommitHook.h @@ -1,9 +1,9 @@ #pragma once #ifdef RCT_NEW_ARCH_ENABLED +#include #include #include -#include #include @@ -14,12 +14,12 @@ using namespace facebook::react; namespace livemarkdown { struct MarkdownTextInputDecoratorPair { - const std::shared_ptr textInput; - const std::shared_ptr decorator; + const std::shared_ptr textInput; + const std::shared_ptr decorator; }; class MarkdownCommitHook : public UIManagerCommitHook { - public: +public: MarkdownCommitHook(const std::shared_ptr &uiManager); ~MarkdownCommitHook() noexcept override; @@ -31,11 +31,10 @@ class MarkdownCommitHook : public UIManagerCommitHook { RootShadowNode::Unshared shadowTreeWillCommit( ShadowTree const &shadowTree, RootShadowNode::Shared const &oldRootShadowNode, - RootShadowNode::Unshared const &newRootShadowNode) - noexcept override; + RootShadowNode::Unshared const &newRootShadowNode) noexcept override; - private: - const std::shared_ptr uiManager_; +private: + const std::shared_ptr uiManager_; }; } // namespace livemarkdown diff --git a/ios/MarkdownCommitHook.mm b/ios/MarkdownCommitHook.mm index 26bb84cc..60123e90 100644 --- a/ios/MarkdownCommitHook.mm +++ b/ios/MarkdownCommitHook.mm @@ -1,8 +1,8 @@ #ifdef RCT_NEW_ARCH_ENABLED +#include #include #include -#include #include "MarkdownCommitHook.h" #include "MarkdownShadowFamilyRegistry.h" @@ -13,7 +13,9 @@ namespace livemarkdown { -MarkdownCommitHook::MarkdownCommitHook(const std::shared_ptr &uiManager) : uiManager_(uiManager) { +MarkdownCommitHook::MarkdownCommitHook( + const std::shared_ptr &uiManager) + : uiManager_(uiManager) { uiManager_->registerCommitHook(*this); } @@ -22,122 +24,168 @@ } RootShadowNode::Unshared MarkdownCommitHook::shadowTreeWillCommit( - ShadowTree const &, - RootShadowNode::Shared const &, + ShadowTree const &, RootShadowNode::Shared const &, RootShadowNode::Unshared const &newRootShadowNode) noexcept { - auto rootNode = newRootShadowNode->ShadowNode::clone(ShadowNodeFragment{}); - - // A preface to why we do the weird thing below: - // On the new architecture there are two ways of measuring text on iOS: by value and by pointer. - // When done by value, the attributed string to be measured is created on the c++ side. We cannot - // modify this process as we do not extend TextInputShadowNode. We also cannot really change the - // layout manager used to do this, since it's a private field (ok, we can but in a not very nice way). - // But also, the logic for parsing and applying markdown is written in JS/ObjC and we really wouldn't - // want to reimplement it in c++. - // - // Nice thing is that it can also be done by pointer to NSAttributedString, which is the platform's - // way to handle styled text, and is also used by Live Markdown. On this path, the measurement is done - // by the OS APIs. The thing we want to make sure of, is that markdown-decorated text input always uses - // this path and uses a pointer to a string with markdown styles applied. Thankfully, RN provides nice - // utility functions that allow to convert between the RN's AttributedString and iOS's NSAttributedString. - // The logic below does exactly that. - - // In order to properly apply markdown formatting to the text input, we need to update the TextInputShadowNode's - // state with styled string, but we only have access to the ShadowNodeFamilies of the decorator components. - // We also know that a markdown decorator is always preceded with the TextInput to decorate, so we need to take - // the sibling. - std::vector nodesToUpdate; - MarkdownShadowFamilyRegistry::runForEveryFamily([&rootNode, &nodesToUpdate](ShadowNodeFamily::Shared family) { - // get the path from the root to the node from the decorator family - const auto ancestors = family->getAncestors(*rootNode); - - if (!ancestors.empty()) { - auto &parentNode = ancestors.back().first.get(); - auto index = ancestors.back().second; - - // this is node represented by one of the registered families and since we only register markdown decorator - // shadow families, static casting should be safe here - const auto& decoratorNode = - std::static_pointer_cast(parentNode.getChildren().at(index)); - // text input always precedes the decorator component - const auto& previousSibling = parentNode.getChildren().at(index - 1); - - if (const auto& textInputNode = std::dynamic_pointer_cast(previousSibling)) { - // store the pair of text input and decorator to update in the next step - // we need both, decorator to get markdown style and text input to update it - nodesToUpdate.push_back({ - textInputNode, - decoratorNode, - }); - } - } - }); - - for (const auto &nodes : nodesToUpdate) { - const auto &textInputState = *std::static_pointer_cast>(nodes.textInput->getState()); - const auto &stateData = textInputState.getData(); - const auto fontSizeMultiplier = newRootShadowNode->getConcreteProps().layoutContext.fontSizeMultiplier; - - // We only want to update the shadow node when the attributed string is stored by value - // If it's stored by pointer, the markdown formatting should already by applied to it, since the - // only source of that pointer (besides this commit hook) is RCTTextInputComponentView, which - // has the relevant method swizzled to make sure the markdown styles are always applied before - // updating state. - // There are two caveats: - // 1. On the first render the swizzled method will not apply markdown since the native component - // is not mounted yet. In that case we save the tag to update in the method applying markdown - // formatting and apply it here instead, preventing wrong layout on reloads. - // 2. When the markdown style prop is changed, the native state needs to be updated to reflect - // them. In that case the relevant tag is saved in the registry when the new shadow node is created. - if (stateData.attributedStringBox.getMode() == AttributedStringBox::Mode::Value || MarkdownShadowFamilyRegistry::shouldForceUpdate(nodes.textInput->getTag())) { - rootNode = rootNode->cloneTree(nodes.textInput->getFamily(), [&nodes, &textInputState, &stateData, fontSizeMultiplier](const ShadowNode& node) { - const auto &markdownProps = *std::static_pointer_cast(nodes.decorator->getProps()); - const auto &textInputProps = *std::static_pointer_cast(nodes.textInput->getProps()); - - const auto defaultTextAttributes = textInputProps.getEffectiveTextAttributes(fontSizeMultiplier); - const auto defaultNSTextAttributes = RCTNSTextAttributesFromTextAttributes(defaultTextAttributes); - - // this can possibly be optimized - RCTMarkdownStyle *markdownStyle = [[RCTMarkdownStyle alloc] initWithStruct:markdownProps.markdownStyle]; - RCTMarkdownUtils *utils = [[RCTMarkdownUtils alloc] init]; - [utils setMarkdownStyle:markdownStyle]; - - // convert the attibuted string stored in state to NSAttributedString - auto nsAttributedString = RCTNSAttributedStringFromAttributedStringBox(stateData.attributedStringBox); - - // Handles the first render, where the text stored in props is different than the one stored in state - // The one in state is empty, while the one in props is passed from JS - // If we don't update the state here, we'll end up with a one-default-line-sized text input. - // A better condition to do that can be probably chosen, but this seems to work - auto plainString = std::string([[nsAttributedString string] UTF8String]); - if (plainString != textInputProps.text) { - // creates new AttributedString from props, adapted from TextInputShadowNode (ios one, text inputs are platform-specific) - auto attributedString = AttributedString{}; - attributedString.appendFragment( - AttributedString::Fragment{textInputProps.text, defaultTextAttributes}); - - auto attachments = BaseTextShadowNode::Attachments{}; - BaseTextShadowNode::buildAttributedString( - defaultTextAttributes, *nodes.textInput, attributedString, attachments); - - // convert the newly created attributed string to NSAttributedString - nsAttributedString = RCTNSAttributedStringFromAttributedStringBox(AttributedStringBox{attributedString}); - } - - // apply markdown - auto newString = [utils parseMarkdown:nsAttributedString withAttributes:defaultNSTextAttributes]; - - // create a clone of the old TextInputState and update the attributed string box to point to the string with markdown applied - auto newStateData = std::make_shared(stateData); - newStateData->attributedStringBox = RCTAttributedStringBoxFromNSAttributedString(newString); - - // clone the text input with the new state - return node.clone({ - .state = std::make_shared>(newStateData, textInputState), - }); - }); + auto rootNode = newRootShadowNode->ShadowNode::clone(ShadowNodeFragment{}); + + // A preface to why we do the weird thing below: + // On the new architecture there are two ways of measuring text on iOS: by + // value and by pointer. When done by value, the attributed string to be + // measured is created on the c++ side. We cannot modify this process as we do + // not extend TextInputShadowNode. We also cannot really change the layout + // manager used to do this, since it's a private field (ok, we can but in a + // not very nice way). But also, the logic for parsing and applying markdown + // is written in JS/ObjC and we really wouldn't want to reimplement it in c++. + // + // Nice thing is that it can also be done by pointer to NSAttributedString, + // which is the platform's way to handle styled text, and is also used by Live + // Markdown. On this path, the measurement is done by the OS APIs. The thing + // we want to make sure of, is that markdown-decorated text input always uses + // this path and uses a pointer to a string with markdown styles applied. + // Thankfully, RN provides nice utility functions that allow to convert + // between the RN's AttributedString and iOS's NSAttributedString. The logic + // below does exactly that. + + // In order to properly apply markdown formatting to the text input, we need + // to update the TextInputShadowNode's state with styled string, but we only + // have access to the ShadowNodeFamilies of the decorator components. We also + // know that a markdown decorator is always preceded with the TextInput to + // decorate, so we need to take the sibling. + std::vector nodesToUpdate; + MarkdownShadowFamilyRegistry::runForEveryFamily([&rootNode, &nodesToUpdate]( + ShadowNodeFamily::Shared + family) { + // get the path from the root to the node from the decorator family + const auto ancestors = family->getAncestors(*rootNode); + + if (!ancestors.empty()) { + auto &parentNode = ancestors.back().first.get(); + auto index = ancestors.back().second; + + // this is node represented by one of the registered families and since we + // only register markdown decorator shadow families, static casting should + // be safe here + const auto &decoratorNode = + std::static_pointer_cast( + parentNode.getChildren().at(index)); + // text input always precedes the decorator component + const auto &previousSibling = parentNode.getChildren().at(index - 1); + + if (const auto &textInputNode = + std::dynamic_pointer_cast( + previousSibling)) { + // store the pair of text input and decorator to update in the next step + // we need both, decorator to get markdown style and text input to + // update it + nodesToUpdate.push_back({ + textInputNode, + decoratorNode, + }); + } + } + }); + + for (const auto &nodes : nodesToUpdate) { + const auto &textInputState = + *std::static_pointer_cast>( + nodes.textInput->getState()); + const auto &stateData = textInputState.getData(); + const auto fontSizeMultiplier = + newRootShadowNode->getConcreteProps().layoutContext.fontSizeMultiplier; + + // We only want to update the shadow node when the attributed string is + // stored by value If it's stored by pointer, the markdown formatting should + // already by applied to it, since the only source of that pointer (besides + // this commit hook) is RCTTextInputComponentView, which has the relevant + // method swizzled to make sure the markdown styles are always applied + // before updating state. There are two caveats: + // 1. On the first render the swizzled method will not apply markdown since + // the native component + // is not mounted yet. In that case we save the tag to update in the + // method applying markdown formatting and apply it here instead, + // preventing wrong layout on reloads. + // 2. When the markdown style prop is changed, the native state needs to be + // updated to reflect + // them. In that case the relevant tag is saved in the registry when the + // new shadow node is created. + if (stateData.attributedStringBox.getMode() == + AttributedStringBox::Mode::Value || + MarkdownShadowFamilyRegistry::shouldForceUpdate( + nodes.textInput->getTag())) { + rootNode = rootNode->cloneTree( + nodes.textInput->getFamily(), + [&nodes, &textInputState, &stateData, + fontSizeMultiplier](const ShadowNode &node) { + const auto &markdownProps = *std::static_pointer_cast< + MarkdownTextInputDecoratorViewProps const>( + nodes.decorator->getProps()); + const auto &textInputProps = + *std::static_pointer_cast( + nodes.textInput->getProps()); + + const auto defaultTextAttributes = + textInputProps.getEffectiveTextAttributes(fontSizeMultiplier); + const auto defaultNSTextAttributes = + RCTNSTextAttributesFromTextAttributes(defaultTextAttributes); + + // this can possibly be optimized + RCTMarkdownStyle *markdownStyle = [[RCTMarkdownStyle alloc] + initWithStruct:markdownProps.markdownStyle]; + RCTMarkdownUtils *utils = [[RCTMarkdownUtils alloc] init]; + [utils setMarkdownStyle:markdownStyle]; + + // convert the attibuted string stored in state to + // NSAttributedString + auto nsAttributedString = + RCTNSAttributedStringFromAttributedStringBox( + stateData.attributedStringBox); + + // Handles the first render, where the text stored in props is + // different than the one stored in state The one in state is empty, + // while the one in props is passed from JS If we don't update the + // state here, we'll end up with a one-default-line-sized text + // input. A better condition to do that can be probably chosen, but + // this seems to work + auto plainString = + std::string([[nsAttributedString string] UTF8String]); + if (plainString != textInputProps.text) { + // creates new AttributedString from props, adapted from + // TextInputShadowNode (ios one, text inputs are + // platform-specific) + auto attributedString = AttributedString{}; + attributedString.appendFragment(AttributedString::Fragment{ + textInputProps.text, defaultTextAttributes}); + + auto attachments = BaseTextShadowNode::Attachments{}; + BaseTextShadowNode::buildAttributedString( + defaultTextAttributes, *nodes.textInput, attributedString, + attachments); + + // convert the newly created attributed string to + // NSAttributedString + nsAttributedString = RCTNSAttributedStringFromAttributedStringBox( + AttributedStringBox{attributedString}); } - } + + // apply markdown + auto newString = [utils parseMarkdown:nsAttributedString + withAttributes:defaultNSTextAttributes]; + + // create a clone of the old TextInputState and update the + // attributed string box to point to the string with markdown + // applied + auto newStateData = std::make_shared(stateData); + newStateData->attributedStringBox = + RCTAttributedStringBoxFromNSAttributedString(newString); + + // clone the text input with the new state + return node.clone({ + .state = std::make_shared>( + newStateData, textInputState), + }); + }); + } + } return std::static_pointer_cast(rootNode); } diff --git a/ios/MarkdownShadowFamilyRegistry.cpp b/ios/MarkdownShadowFamilyRegistry.cpp index 792b31ff..d41d1726 100644 --- a/ios/MarkdownShadowFamilyRegistry.cpp +++ b/ios/MarkdownShadowFamilyRegistry.cpp @@ -2,49 +2,57 @@ #include "MarkdownShadowFamilyRegistry.h" -std::set MarkdownShadowFamilyRegistry::familiesToUpdate_; +std::set + MarkdownShadowFamilyRegistry::familiesToUpdate_; std::set MarkdownShadowFamilyRegistry::forcedUpdates_; std::mutex MarkdownShadowFamilyRegistry::mutex_; -void MarkdownShadowFamilyRegistry::registerFamilyForUpdates(facebook::react::ShadowNodeFamily::Shared family) -{ - auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::mutex_); - MarkdownShadowFamilyRegistry::familiesToUpdate_.insert(family); +void MarkdownShadowFamilyRegistry::registerFamilyForUpdates( + facebook::react::ShadowNodeFamily::Shared family) { + auto lock = + std::unique_lock(MarkdownShadowFamilyRegistry::mutex_); + MarkdownShadowFamilyRegistry::familiesToUpdate_.insert(family); } -void MarkdownShadowFamilyRegistry::unregisterFamilyForUpdates(facebook::react::ShadowNodeFamily::Shared family) -{ - auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::mutex_); - MarkdownShadowFamilyRegistry::familiesToUpdate_.erase(family); +void MarkdownShadowFamilyRegistry::unregisterFamilyForUpdates( + facebook::react::ShadowNodeFamily::Shared family) { + auto lock = + std::unique_lock(MarkdownShadowFamilyRegistry::mutex_); + MarkdownShadowFamilyRegistry::familiesToUpdate_.erase(family); } void MarkdownShadowFamilyRegistry::reset() { - auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::mutex_); - MarkdownShadowFamilyRegistry::familiesToUpdate_.clear(); - MarkdownShadowFamilyRegistry::forcedUpdates_.clear(); + auto lock = + std::unique_lock(MarkdownShadowFamilyRegistry::mutex_); + MarkdownShadowFamilyRegistry::familiesToUpdate_.clear(); + MarkdownShadowFamilyRegistry::forcedUpdates_.clear(); } -void MarkdownShadowFamilyRegistry::runForEveryFamily(std::function fun) -{ - auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::mutex_); - for (auto &family : MarkdownShadowFamilyRegistry::familiesToUpdate_) { - fun(family); - } +void MarkdownShadowFamilyRegistry::runForEveryFamily( + std::function fun) { + auto lock = + std::unique_lock(MarkdownShadowFamilyRegistry::mutex_); + for (auto &family : MarkdownShadowFamilyRegistry::familiesToUpdate_) { + fun(family); + } } -void MarkdownShadowFamilyRegistry::forceNextStateUpdate(facebook::react::Tag tag) { - auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::mutex_); - forcedUpdates_.insert(tag); +void MarkdownShadowFamilyRegistry::forceNextStateUpdate( + facebook::react::Tag tag) { + auto lock = + std::unique_lock(MarkdownShadowFamilyRegistry::mutex_); + forcedUpdates_.insert(tag); } bool MarkdownShadowFamilyRegistry::shouldForceUpdate(facebook::react::Tag tag) { - auto lock = std::unique_lock(MarkdownShadowFamilyRegistry::mutex_); - bool force = forcedUpdates_.contains(tag); - if (force) { - forcedUpdates_.erase(tag); - return true; - } - return false; + auto lock = + std::unique_lock(MarkdownShadowFamilyRegistry::mutex_); + bool force = forcedUpdates_.contains(tag); + if (force) { + forcedUpdates_.erase(tag); + return true; + } + return false; } #endif diff --git a/ios/MarkdownShadowFamilyRegistry.h b/ios/MarkdownShadowFamilyRegistry.h index 49c10688..6d6195da 100644 --- a/ios/MarkdownShadowFamilyRegistry.h +++ b/ios/MarkdownShadowFamilyRegistry.h @@ -6,25 +6,30 @@ #include #include -// A registry to store pointers to the ShadowNodeFamilies of markdown decorators. -// The only place we can _legally_ access the family of shadow node is in the constructor -// and we need it inside commit hook. To achieve it, we use this simple registry where families -// are registered when nodes are created and cleaned up when native view is removed from window -// or when a turbomodule is deallocated. +// A registry to store pointers to the ShadowNodeFamilies of markdown +// decorators. The only place we can _legally_ access the family of shadow node +// is in the constructor and we need it inside commit hook. To achieve it, we +// use this simple registry where families are registered when nodes are created +// and cleaned up when native view is removed from window or when a turbomodule +// is deallocated. class MarkdownShadowFamilyRegistry { public: - static void registerFamilyForUpdates(facebook::react::ShadowNodeFamily::Shared family); - static void unregisterFamilyForUpdates(facebook::react::ShadowNodeFamily::Shared family); - static void reset(); - static void runForEveryFamily(std::function fun); - static void forceNextStateUpdate(facebook::react::Tag tag); - static bool shouldForceUpdate(facebook::react::Tag tag); + static void + registerFamilyForUpdates(facebook::react::ShadowNodeFamily::Shared family); + static void + unregisterFamilyForUpdates(facebook::react::ShadowNodeFamily::Shared family); + static void reset(); + static void runForEveryFamily( + std::function + fun); + static void forceNextStateUpdate(facebook::react::Tag tag); + static bool shouldForceUpdate(facebook::react::Tag tag); private: - static std::set familiesToUpdate_; - static std::set forcedUpdates_; - static std::mutex mutex_; + static std::set familiesToUpdate_; + static std::set forcedUpdates_; + static std::mutex mutex_; }; #endif diff --git a/ios/MarkdownTextInputDecoratorComponentDescriptor.h b/ios/MarkdownTextInputDecoratorComponentDescriptor.h index ab2901ea..2b026647 100644 --- a/ios/MarkdownTextInputDecoratorComponentDescriptor.h +++ b/ios/MarkdownTextInputDecoratorComponentDescriptor.h @@ -1,7 +1,7 @@ #pragma once -#include #include "MarkdownTextInputDecoratorShadowNode.h" +#include #include namespace facebook { @@ -9,10 +9,9 @@ namespace react { class MarkdownTextInputDecoratorComponentDescriptor final : public ConcreteComponentDescriptor { - public: +public: using ConcreteComponentDescriptor::ConcreteComponentDescriptor; }; } // namespace react } // namespace facebook - diff --git a/ios/MarkdownTextInputDecoratorShadowNode.cpp b/ios/MarkdownTextInputDecoratorShadowNode.cpp index ceaa76fb..4ea2cc04 100644 --- a/ios/MarkdownTextInputDecoratorShadowNode.cpp +++ b/ios/MarkdownTextInputDecoratorShadowNode.cpp @@ -2,27 +2,34 @@ #include -#include "MarkdownTextInputDecoratorShadowNode.h" #include "MarkdownShadowFamilyRegistry.h" +#include "MarkdownTextInputDecoratorShadowNode.h" namespace facebook { namespace react { -extern const char MarkdownTextInputDecoratorViewComponentName[] = "MarkdownTextInputDecoratorView"; - -const ShadowNodeFragment::Value MarkdownTextInputDecoratorShadowNode::updateFragmentState(ShadowNodeFragment const &fragment, ShadowNodeFamily::Shared const &family) { - const auto newStateData = std::make_shared(family); - - MarkdownShadowFamilyRegistry::registerFamilyForUpdates(family); - - // we pass the pointer to the ShadowNodeFamily in the initial state, so it's propagated on every clone - // we need it to clear the reference in the registry when the view is removed from window - // it cannot be done in the destructor, as multiple shadow nodes for the same family may be created - return ShadowNodeFragment::Value({ - .props = fragment.props, - .children = fragment.children, - .state = std::make_shared(newStateData, *fragment.state), - }); +extern const char MarkdownTextInputDecoratorViewComponentName[] = + "MarkdownTextInputDecoratorView"; + +const ShadowNodeFragment::Value +MarkdownTextInputDecoratorShadowNode::updateFragmentState( + ShadowNodeFragment const &fragment, + ShadowNodeFamily::Shared const &family) { + const auto newStateData = + std::make_shared(family); + + MarkdownShadowFamilyRegistry::registerFamilyForUpdates(family); + + // we pass the pointer to the ShadowNodeFamily in the initial state, so it's + // propagated on every clone we need it to clear the reference in the registry + // when the view is removed from window it cannot be done in the destructor, + // as multiple shadow nodes for the same family may be created + return ShadowNodeFragment::Value({ + .props = fragment.props, + .children = fragment.children, + .state = + std::make_shared(newStateData, *fragment.state), + }); } } // namespace react diff --git a/ios/MarkdownTextInputDecoratorShadowNode.h b/ios/MarkdownTextInputDecoratorShadowNode.h index 7187cdc4..8dbca525 100644 --- a/ios/MarkdownTextInputDecoratorShadowNode.h +++ b/ios/MarkdownTextInputDecoratorShadowNode.h @@ -1,44 +1,46 @@ #pragma once #ifdef RCT_NEW_ARCH_ENABLED -#include "MarkdownTextInputDecoratorState.h" #include "MarkdownShadowFamilyRegistry.h" +#include "MarkdownTextInputDecoratorState.h" +#include #include #include -#include #include -#include +#include namespace facebook { namespace react { JSI_EXPORT extern const char MarkdownTextInputDecoratorViewComponentName[]; - -class JSI_EXPORT MarkdownTextInputDecoratorShadowNode final : public ConcreteViewShadowNode< - MarkdownTextInputDecoratorViewComponentName, - MarkdownTextInputDecoratorViewProps, - MarkdownTextInputDecoratorViewEventEmitter, - MarkdownTextInputDecoratorState> { - public: - MarkdownTextInputDecoratorShadowNode( - ShadowNodeFragment const &fragment, - ShadowNodeFamily::Shared const &family, - ShadowNodeTraits traits) - : ConcreteViewShadowNode(static_cast(updateFragmentState(fragment, family)), family, traits) {} - - MarkdownTextInputDecoratorShadowNode( - ShadowNode const &sourceShadowNode, - ShadowNodeFragment const &fragment) - : ConcreteViewShadowNode(sourceShadowNode, fragment) { - // if the props changed, we need to update the shadow node state to reflect potential style changes - if (fragment.props != ShadowNodeFragment::propsPlaceholder()) { - MarkdownShadowFamilyRegistry::forceNextStateUpdate(this->getTag()); - } - } - - private: - static const ShadowNodeFragment::Value updateFragmentState(ShadowNodeFragment const &fragment, ShadowNodeFamily::Shared const &family); +class JSI_EXPORT MarkdownTextInputDecoratorShadowNode final + : public ConcreteViewShadowNode { +public: + MarkdownTextInputDecoratorShadowNode(ShadowNodeFragment const &fragment, + ShadowNodeFamily::Shared const &family, + ShadowNodeTraits traits) + : ConcreteViewShadowNode(static_cast( + updateFragmentState(fragment, family)), + family, traits) {} + + MarkdownTextInputDecoratorShadowNode(ShadowNode const &sourceShadowNode, + ShadowNodeFragment const &fragment) + : ConcreteViewShadowNode(sourceShadowNode, fragment) { + // if the props changed, we need to update the shadow node state to reflect + // potential style changes + if (fragment.props != ShadowNodeFragment::propsPlaceholder()) { + MarkdownShadowFamilyRegistry::forceNextStateUpdate(this->getTag()); + } + } + +private: + static const ShadowNodeFragment::Value + updateFragmentState(ShadowNodeFragment const &fragment, + ShadowNodeFamily::Shared const &family); }; } // namespace react diff --git a/ios/MarkdownTextInputDecoratorState.h b/ios/MarkdownTextInputDecoratorState.h index 67c77a8b..cf6b292f 100644 --- a/ios/MarkdownTextInputDecoratorState.h +++ b/ios/MarkdownTextInputDecoratorState.h @@ -6,11 +6,13 @@ namespace facebook { namespace react { class JSI_EXPORT MarkdownTextInputDecoratorState final { - public: +public: using Shared = std::shared_ptr; - MarkdownTextInputDecoratorState() : decoratorFamily(nullptr) {}; - MarkdownTextInputDecoratorState(const ShadowNodeFamily::Shared textInputFamily_) : decoratorFamily(textInputFamily_) {}; + MarkdownTextInputDecoratorState() : decoratorFamily(nullptr){}; + MarkdownTextInputDecoratorState( + const ShadowNodeFamily::Shared textInputFamily_) + : decoratorFamily(textInputFamily_){}; const ShadowNodeFamily::Shared decoratorFamily; }; diff --git a/ios/RCTLiveMarkdownModule.h b/ios/RCTLiveMarkdownModule.h index 99cfdc0b..ceee8fef 100644 --- a/ios/RCTLiveMarkdownModule.h +++ b/ios/RCTLiveMarkdownModule.h @@ -1,12 +1,12 @@ #ifdef RCT_NEW_ARCH_ENABLED -#import -#import #import +#import +#import // Without inheriting after RCTEventEmitter we don't get access to bridge -@interface RCTLiveMarkdownModule : RCTEventEmitter +@interface RCTLiveMarkdownModule + : RCTEventEmitter @end #endif // RCT_NEW_ARCH_ENABLED - diff --git a/ios/RCTLiveMarkdownModule.mm b/ios/RCTLiveMarkdownModule.mm index 881e0da3..fa6eb39e 100644 --- a/ios/RCTLiveMarkdownModule.mm +++ b/ios/RCTLiveMarkdownModule.mm @@ -1,48 +1,48 @@ #ifdef RCT_NEW_ARCH_ENABLED -#import #import +#import -#import "RCTLiveMarkdownModule.h" #import "MarkdownCommitHook.h" #import "MarkdownShadowFamilyRegistry.h" +#import "RCTLiveMarkdownModule.h" // A turbomodule used to register the commit hook -// I think this is the easiest way to access the UIManager, which we need to actually register the hook +// I think this is the easiest way to access the UIManager, which we need to +// actually register the hook @implementation RCTLiveMarkdownModule { - BOOL installed_; - std::shared_ptr commitHook_; + BOOL installed_; + std::shared_ptr commitHook_; } RCT_EXPORT_MODULE(@"LiveMarkdownModule") -- (NSNumber *)install -{ - if (!installed_) { - installed_ = YES; - - RCTBridge *bridge = self.bridge; - RCTSurfacePresenter *surfacePresenter = bridge.surfacePresenter; - RCTScheduler *scheduler = [surfacePresenter scheduler]; - - commitHook_ = std::make_shared(scheduler.uiManager); - } - return @1; +- (NSNumber *)install { + if (!installed_) { + installed_ = YES; + + RCTBridge *bridge = self.bridge; + RCTSurfacePresenter *surfacePresenter = bridge.surfacePresenter; + RCTScheduler *scheduler = [surfacePresenter scheduler]; + + commitHook_ = + std::make_shared(scheduler.uiManager); + } + return @1; } -- (std::shared_ptr)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params -{ - return std::make_shared(params); +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params { + return std::make_shared( + params); } -- (void)invalidate -{ - MarkdownShadowFamilyRegistry::reset(); - [super invalidate]; +- (void)invalidate { + MarkdownShadowFamilyRegistry::reset(); + [super invalidate]; } @end #endif // RCT_NEW_ARCH_ENABLED - From ce9ffa55b6684263725153a4b900f4de7084a67c Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 9 Apr 2024 09:45:08 +0200 Subject: [PATCH 33/35] Use correct cocoapods version --- example/ios/Podfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index b14effe1..f2060173 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1377,4 +1377,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 8cb8ab8858b4911d497d269a353fbfff868afef0 -COCOAPODS: 1.15.2 +COCOAPODS: 1.14.3 From 1ef6f88cd423845dd6906460a853b727fe3ef3ad Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 9 Apr 2024 11:35:26 +0200 Subject: [PATCH 34/35] Fix new arch compilation after main merge --- ios/MarkdownTextInputDecoratorView.mm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ios/MarkdownTextInputDecoratorView.mm b/ios/MarkdownTextInputDecoratorView.mm index 526e0f70..0cec2191 100644 --- a/ios/MarkdownTextInputDecoratorView.mm +++ b/ios/MarkdownTextInputDecoratorView.mm @@ -100,7 +100,11 @@ - (void)setMarkdownStyle:(RCTMarkdownStyle *)markdownStyle { _markdownStyle = markdownStyle; [_markdownUtils setMarkdownStyle:markdownStyle]; +#ifdef RCT_NEW_ARCH_ENABLED + [_textInput textInputDidChange]; // apply new styles +#else [_textInput setAttributedText:_textInput.attributedText]; // apply new styles +#endif } @end From 6f11c78d4dfa799be41018f705ecaa1fd3209146 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Tue, 9 Apr 2024 17:30:22 +0200 Subject: [PATCH 35/35] Make sure it works with frameworks --- RNLiveMarkdown.podspec | 7 +++++++ example/ios/Podfile.lock | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/RNLiveMarkdown.podspec b/RNLiveMarkdown.podspec index f2d90380..a892205c 100644 --- a/RNLiveMarkdown.podspec +++ b/RNLiveMarkdown.podspec @@ -19,4 +19,11 @@ Pod::Spec.new do |s| s.resources = "parser/react-native-live-markdown-parser.js" install_modules_dependencies(s) + + if ENV['USE_FRAMEWORKS'] && ENV['RCT_NEW_ARCH_ENABLED'] + add_dependency(s, "React-Fabric", :additional_framework_paths => [ + "react/renderer/textlayoutmanager/platform/ios", + "react/renderer/components/textinput/iostextinput", + ]) + end end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 0b14a8f6..2fb37347 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1111,7 +1111,7 @@ PODS: - React-jsi (= 0.73.4) - React-logger (= 0.73.4) - React-perflogger (= 0.73.4) - - RNLiveMarkdown (0.1.43): + - RNLiveMarkdown (0.1.44): - glog - RCT-Folly (= 2022.05.16.00) - React-Core @@ -1371,7 +1371,7 @@ SPEC CHECKSUMS: React-runtimescheduler: ed48e5faac6751e66ee1261c4bd01643b436f112 React-utils: 6e5ad394416482ae21831050928ae27348f83487 ReactCommon: 840a955d37b7f3358554d819446bffcf624b2522 - RNLiveMarkdown: a63967738fd835165f740453942c193275d85936 + RNLiveMarkdown: b0fe5fbcfa24b0dc190b6d246793d528a1c7c452 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70