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