From 667f69a185ce482c4c162279fe8f9450cfc70ff0 Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki <tomekzawadzki98@gmail.com> Date: Tue, 2 Jan 2024 16:54:24 +0100 Subject: [PATCH 1/5] Use Hermes instead of JSC on iOS --- example/ios/Podfile.lock | 3 ++- ios/RCTMarkdownUtils.mm | 32 +++++++++++++++++------- react-native-markdown-text-input.podspec | 2 ++ 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 379a58fc6..23ec70cf3 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -376,6 +376,7 @@ PODS: - React-logger (0.72.7): - glog - react-native-markdown-text-input (0.1.0): + - hermes-engine - RCT-Folly (= 2021.07.22.00) - React-Core - React-NativeModulesApple (0.72.7): @@ -693,7 +694,7 @@ SPEC CHECKSUMS: React-jsiexecutor: c49502e5d02112247ee4526bc3ccfc891ae3eb9b React-jsinspector: 8baadae51f01d867c3921213a25ab78ab4fbcd91 React-logger: 8edc785c47c8686c7962199a307015e2ce9a0e4f - react-native-markdown-text-input: 075defe331854ed02422c4c82125d212b92732f6 + react-native-markdown-text-input: 04225511887a621e1e70bcb3516b2463bd4f3c52 React-NativeModulesApple: b6868ee904013a7923128892ee4a032498a1024a React-perflogger: 31ea61077185eb1428baf60c0db6e2886f141a5a React-RCTActionSheet: 392090a3abc8992eb269ef0eaa561750588fc39d diff --git a/ios/RCTMarkdownUtils.mm b/ios/RCTMarkdownUtils.mm index e8a8a3cc6..539a92500 100644 --- a/ios/RCTMarkdownUtils.mm +++ b/ios/RCTMarkdownUtils.mm @@ -1,6 +1,11 @@ #import <react-native-markdown-text-input/RCTMarkdownUtils.h> #import <JavaScriptCore/JavaScriptCore.h> +#include <jsi/jsi.h> +#include <hermes/hermes.h> + +using namespace facebook; + static UIColor *syntaxColor = [UIColor grayColor]; static UIColor *linkColor = [UIColor blueColor]; static UIColor *codeForegroundColor = [[UIColor alloc] initWithRed:6/255.0 green:25/255.0 blue:109/255.0 alpha:1.0]; @@ -32,21 +37,30 @@ - (NSAttributedString *)parseMarkdown:(NSAttributedString *)input { return _prevAttributedString; } - static JSContext *ctx = nil; - static JSValue *function = nil; - if (ctx == nil) { + static std::shared_ptr<jsi::Runtime> runtime; + if (runtime == nullptr) { NSString *path = [[NSBundle mainBundle] pathForResource:@"out" ofType:@"js"]; assert(path != nil && "[react-native-markdown-text-input] Markdown parser bundle not found"); NSString *content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL]; assert(content != nil && "[react-native-markdown-text-input] Markdown parser bundle is empty"); - ctx = [[JSContext alloc] init]; - [ctx evaluateScript:content]; - function = ctx[@"parseMarkdownToTextAndRanges"]; + runtime = facebook::hermes::makeHermesRuntime(); + auto codeBuffer = std::make_shared<const jsi::StringBuffer>([content UTF8String]); + runtime->evaluateJavaScript(codeBuffer, "nativeInitializeRuntime"); } - JSValue *result = [function callWithArguments:@[inputString]]; - NSString *outputString = [result[0] toString]; - NSArray *ranges = [result[1] toArray]; + jsi::Runtime &rt = *runtime; + auto func = rt.global().getPropertyAsFunction(rt, "parseMarkdownToTextAndRanges"); + auto output = func.call(rt, [inputString UTF8String]); + auto json = rt.global().getPropertyAsObject(rt, "JSON").getPropertyAsFunction(rt, "stringify").call(rt, output).asString(rt).utf8(rt); + + NSError *error = nil; + NSData *data = [NSData dataWithBytes:json.data() length:json.length()]; + NSArray *result = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + if (error != nil) { + return input; + } + NSString *outputString = result[0]; + NSArray *ranges = result[1]; if (![outputString isEqualToString:inputString]) { return input; diff --git a/react-native-markdown-text-input.podspec b/react-native-markdown-text-input.podspec index 1d7ef1d81..0ae634b2d 100644 --- a/react-native-markdown-text-input.podspec +++ b/react-native-markdown-text-input.podspec @@ -18,6 +18,8 @@ Pod::Spec.new do |s| s.resources = "parser/out.js" + s.dependency "hermes-engine" + # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79. if respond_to?(:install_modules_dependencies, true) From bd535ef80cbb0ab9815017efe3094eb1342bedd1 Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki <tomekzawadzki98@gmail.com> Date: Mon, 8 Jan 2024 18:45:45 +0100 Subject: [PATCH 2/5] Remove import --- ios/RCTMarkdownUtils.mm | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ios/RCTMarkdownUtils.mm b/ios/RCTMarkdownUtils.mm index 1708c98d9..d46019a78 100644 --- a/ios/RCTMarkdownUtils.mm +++ b/ios/RCTMarkdownUtils.mm @@ -1,9 +1,8 @@ #import <react-native-markdown-text-input/RCTMarkdownUtils.h> #import <react/debug/react_native_assert.h> #import <React/RCTAssert.h> -#import <JavaScriptCore/JavaScriptCore.h> -#include <jsi/jsi.h> #include <hermes/hermes.h> +#include <jsi/jsi.h> using namespace facebook; @@ -50,7 +49,7 @@ - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input auto func = rt.global().getPropertyAsFunction(rt, "parseExpensiMarkToRanges"); auto output = func.call(rt, [inputString UTF8String]); auto json = rt.global().getPropertyAsObject(rt, "JSON").getPropertyAsFunction(rt, "stringify").call(rt, output).asString(rt).utf8(rt); - + NSData *data = [NSData dataWithBytes:json.data() length:json.length()]; NSError *error = nil; NSArray *ranges = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; From e29089e12f3f79e1897d938361cfcccd74c8a378 Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki <tomekzawadzki98@gmail.com> Date: Wed, 26 Jun 2024 14:34:54 +0200 Subject: [PATCH 3/5] Use Hermes instead of JavaScriptCore on iOS --- RNLiveMarkdown.podspec | 2 ++ example/src/App.tsx | 4 +--- ios/RCTMarkdownUtils.mm | 30 +++++++++++++++++++++--------- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/RNLiveMarkdown.podspec b/RNLiveMarkdown.podspec index 781f3d49c..b1620ad13 100644 --- a/RNLiveMarkdown.podspec +++ b/RNLiveMarkdown.podspec @@ -18,6 +18,8 @@ Pod::Spec.new do |s| s.resources = "parser/react-native-live-markdown-parser.js" + s.dependency "hermes-engine" + install_modules_dependencies(s) if ENV['USE_FRAMEWORKS'] && ENV['RCT_NEW_ARCH_ENABLED'] diff --git a/example/src/App.tsx b/example/src/App.tsx index 71648f563..c59e07a2e 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -4,7 +4,6 @@ import {Button, Platform, StyleSheet, Text, View} from 'react-native'; import {MarkdownTextInput} from '@expensify/react-native-live-markdown'; import type {TextInput} from 'react-native'; -import * as TEST_CONST from '../../WebExample/__tests__/testConstants'; function isWeb() { return Platform.OS === 'web'; @@ -59,7 +58,7 @@ function getRandomColor() { } export default function App() { - const [value, setValue] = React.useState(TEST_CONST.EXAMPLE_CONTENT); + const [value, setValue] = React.useState('Hello *world*!'); const [markdownStyle, setMarkdownStyle] = React.useState({}); const [selection, setSelection] = React.useState({start: 0, end: 0}); @@ -100,7 +99,6 @@ export default function App() { placeholder="Type here..." onSelectionChange={(e) => setSelection(e.nativeEvent.selection)} selection={selection} - id={TEST_CONST.INPUT_ID} /> {/* <Text>TextInput singleline</Text> <TextInput diff --git a/ios/RCTMarkdownUtils.mm b/ios/RCTMarkdownUtils.mm index f188429a2..9c434dd5a 100644 --- a/ios/RCTMarkdownUtils.mm +++ b/ios/RCTMarkdownUtils.mm @@ -2,7 +2,11 @@ #import "react_native_assert.h" #import <React/RCTAssert.h> #import <React/RCTFont.h> -#import <JavaScriptCore/JavaScriptCore.h> + +#include <jsi/jsi.h> +#include <hermes/hermes.h> + +using namespace facebook; @implementation RCTMarkdownUtils { NSString *_prevInputString; @@ -23,20 +27,28 @@ - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withA return _prevAttributedString; } - static JSContext *ctx = nil; - static JSValue *function = nil; - if (ctx == nil) { + static std::shared_ptr<jsi::Runtime> runtime; + if (runtime == nullptr) { 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"]; + runtime = facebook::hermes::makeHermesRuntime(); + auto codeBuffer = std::make_shared<const jsi::StringBuffer>([content UTF8String]); + runtime->evaluateJavaScript(codeBuffer, "nativeInitializeRuntime"); } - JSValue *result = [function callWithArguments:@[inputString]]; - NSArray *ranges = [result toArray]; + jsi::Runtime &rt = *runtime; + auto func = rt.global().getPropertyAsFunction(rt, "parseExpensiMarkToRanges"); + auto output = func.call(rt, [inputString UTF8String]); + auto json = rt.global().getPropertyAsObject(rt, "JSON").getPropertyAsFunction(rt, "stringify").call(rt, output).asString(rt).utf8(rt); + + NSData *data = [NSData dataWithBytes:json.data() length:json.length()]; + NSError *error = nil; + NSArray *ranges = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + if (error != nil) { + return input; + } NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:inputString attributes:attributes]; [attributedString beginEditing]; From 1440486f1624a2860c50d752dee99a5fd30e938d Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki <tomekzawadzki98@gmail.com> Date: Wed, 26 Jun 2024 20:45:57 +0200 Subject: [PATCH 4/5] Eliminate conversion to Obj-C objects --- ios/RCTMarkdownUtils.mm | 63 ++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/ios/RCTMarkdownUtils.mm b/ios/RCTMarkdownUtils.mm index 9c434dd5a..2ce276de9 100644 --- a/ios/RCTMarkdownUtils.mm +++ b/ios/RCTMarkdownUtils.mm @@ -35,20 +35,18 @@ - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withA assert(content != nil && "[react-native-live-markdown] Markdown parser bundle is empty"); runtime = facebook::hermes::makeHermesRuntime(); auto codeBuffer = std::make_shared<const jsi::StringBuffer>([content UTF8String]); - runtime->evaluateJavaScript(codeBuffer, "nativeInitializeRuntime"); + runtime->evaluateJavaScript(codeBuffer, "evaluateJavaScript"); } jsi::Runtime &rt = *runtime; - auto func = rt.global().getPropertyAsFunction(rt, "parseExpensiMarkToRanges"); - auto output = func.call(rt, [inputString UTF8String]); - auto json = rt.global().getPropertyAsObject(rt, "JSON").getPropertyAsFunction(rt, "stringify").call(rt, output).asString(rt).utf8(rt); + auto text = jsi::String::createFromUtf8(rt, [inputString UTF8String]); - NSData *data = [NSData dataWithBytes:json.data() length:json.length()]; - NSError *error = nil; - NSArray *ranges = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; - if (error != nil) { + auto func = rt.global().getPropertyAsFunction(rt, "parseExpensiMarkToRanges"); + auto output = func.call(rt, text); + if (output.isUndefined()) { return input; } + const auto &ranges = output.asObject(rt).asArray(rt); NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:inputString attributes:attributes]; [attributedString beginEditing]; @@ -60,42 +58,43 @@ - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withA _blockquoteRangesAndLevels = [NSMutableArray new]; - [ranges enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - NSDictionary *item = obj; - NSString *type = [item valueForKey:@"type"]; - NSInteger location = [[item valueForKey:@"start"] unsignedIntegerValue]; - NSInteger length = [[item valueForKey:@"length"] unsignedIntegerValue]; - NSInteger depth = [[item valueForKey:@"depth"] unsignedIntegerValue] ?: 1; + for (size_t i = 0, n = ranges.size(rt); i < n; ++i) { + const auto &item = ranges.getValueAtIndex(rt, i).asObject(rt); + const auto &type = item.getProperty(rt, "type").asString(rt).utf8(rt); + const auto &location = static_cast<int>(item.getProperty(rt, "start").asNumber()); + const auto &length = static_cast<int>(item.getProperty(rt, "length").asNumber()); + const auto &depth = item.hasProperty(rt, "depth") ? static_cast<int>(item.getProperty(rt, "depth").asNumber()) : 1; + NSRange range = NSMakeRange(location, length); - if ([type isEqualToString:@"bold"] || [type isEqualToString:@"italic"] || [type isEqualToString:@"code"] || [type isEqualToString:@"pre"] || [type isEqualToString:@"h1"] || [type isEqualToString:@"emoji"]) { + if (type == "bold" || type == "italic" || type == "code" || type == "pre" || type == "h1" || type == "emoji") { UIFont *font = [attributedString attribute:NSFontAttributeName atIndex:location effectiveRange:NULL]; - if ([type isEqualToString:@"bold"]) { + if (type == "bold") { font = [RCTFont updateFont:font withWeight:@"bold"]; - } else if ([type isEqualToString:@"italic"]) { + } else if (type == "italic") { font = [RCTFont updateFont:font withStyle:@"italic"]; - } else if ([type isEqualToString:@"code"]) { + } else if (type == "code") { font = [RCTFont updateFont:font withFamily:_markdownStyle.codeFontFamily size:[NSNumber numberWithFloat:_markdownStyle.codeFontSize] weight:nil style:nil variant:nil scaleMultiplier:0]; - } else if ([type isEqualToString:@"pre"]) { + } else if (type == "pre") { font = [RCTFont updateFont:font withFamily:_markdownStyle.preFontFamily size:[NSNumber numberWithFloat:_markdownStyle.preFontSize] weight:nil style:nil variant:nil scaleMultiplier:0]; - } else if ([type isEqualToString:@"h1"]) { + } else if (type == "h1") { font = [RCTFont updateFont:font withFamily:nil size:[NSNumber numberWithFloat:_markdownStyle.h1FontSize] weight:@"bold" style:nil variant:nil scaleMultiplier:0]; - } else if ([type isEqualToString:@"emoji"]) { + } else if (type == "emoji") { font = [RCTFont updateFont:font withFamily:nil size:[NSNumber numberWithFloat:_markdownStyle.emojiFontSize] weight:nil @@ -106,27 +105,27 @@ - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withA [attributedString addAttribute:NSFontAttributeName value:font range:range]; } - if ([type isEqualToString:@"syntax"]) { + if (type == "syntax") { [attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.syntaxColor range:range]; - } else if ([type isEqualToString:@"strikethrough"]) { + } else if (type == "strikethrough") { [attributedString addAttribute:NSStrikethroughStyleAttributeName value:[NSNumber numberWithInteger:NSUnderlineStyleSingle] range:range]; - } else if ([type isEqualToString:@"code"]) { + } else if (type == "code") { [attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.codeColor range:range]; [attributedString addAttribute:NSBackgroundColorAttributeName value:_markdownStyle.codeBackgroundColor range:range]; - } else if ([type isEqualToString:@"mention-here"]) { + } else if (type == "mention-here") { [attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.mentionHereColor range:range]; [attributedString addAttribute:NSBackgroundColorAttributeName value:_markdownStyle.mentionHereBackgroundColor range:range]; - } else if ([type isEqualToString:@"mention-user"]) { + } else if (type == "mention-user") { // TODO: change mention color when it mentions current user [attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.mentionUserColor range:range]; [attributedString addAttribute:NSBackgroundColorAttributeName value:_markdownStyle.mentionUserBackgroundColor range:range]; - } else if ([type isEqualToString:@"mention-report"]) { + } else if (type == "mention-report") { [attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.mentionReportColor range:range]; [attributedString addAttribute:NSBackgroundColorAttributeName value:_markdownStyle.mentionReportBackgroundColor range:range]; - } else if ([type isEqualToString:@"link"]) { + } else if (type == "link") { [attributedString addAttribute:NSUnderlineStyleAttributeName value:[NSNumber numberWithInteger:NSUnderlineStyleSingle] range:range]; [attributedString addAttribute:NSForegroundColorAttributeName value:_markdownStyle.linkColor range:range]; - } else if ([type isEqualToString:@"blockquote"]) { + } else if (type == "blockquote") { CGFloat indent = (_markdownStyle.blockquoteMarginLeft + _markdownStyle.blockquoteBorderWidth + _markdownStyle.blockquotePaddingLeft) * depth; NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; paragraphStyle.firstLineHeadIndent = indent; @@ -136,17 +135,17 @@ - (NSAttributedString *)parseMarkdown:(nullable NSAttributedString *)input withA @"range": [NSValue valueWithRange:range], @"depth": @(depth) }]; - } else if ([type isEqualToString:@"pre"]) { + } else if (type == "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"]) { + } else if (type == "h1") { NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; NSRange rangeWithHashAndSpace = NSMakeRange(range.location - 2, range.length + 2); // we also need to include prepending "# " [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:rangeWithHashAndSpace]; } - }]; + } RCTApplyBaselineOffset(attributedString); From 185d2b3db8b652458377905eccd51a1ee5323780 Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki <tomekzawadzki98@gmail.com> Date: Wed, 26 Jun 2024 20:54:03 +0200 Subject: [PATCH 5/5] Restore original App.tsx --- example/src/App.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index c59e07a2e..71648f563 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -4,6 +4,7 @@ import {Button, Platform, StyleSheet, Text, View} from 'react-native'; import {MarkdownTextInput} from '@expensify/react-native-live-markdown'; import type {TextInput} from 'react-native'; +import * as TEST_CONST from '../../WebExample/__tests__/testConstants'; function isWeb() { return Platform.OS === 'web'; @@ -58,7 +59,7 @@ function getRandomColor() { } export default function App() { - const [value, setValue] = React.useState('Hello *world*!'); + const [value, setValue] = React.useState(TEST_CONST.EXAMPLE_CONTENT); const [markdownStyle, setMarkdownStyle] = React.useState({}); const [selection, setSelection] = React.useState({start: 0, end: 0}); @@ -99,6 +100,7 @@ export default function App() { placeholder="Type here..." onSelectionChange={(e) => setSelection(e.nativeEvent.selection)} selection={selection} + id={TEST_CONST.INPUT_ID} /> {/* <Text>TextInput singleline</Text> <TextInput