@expensify/react-native-live-markdown 0.1.187 → 0.1.188

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +55 -8
  2. package/RNLiveMarkdown.podspec +2 -4
  3. package/android/build.gradle +4 -9
  4. package/android/src/main/cpp/CMakeLists.txt +14 -6
  5. package/android/src/main/cpp/MarkdownUtils.cpp +13 -24
  6. package/android/src/main/cpp/MarkdownUtils.h +2 -7
  7. package/android/src/main/cpp/OnLoad.cpp +6 -0
  8. package/android/src/main/java/com/expensify/livemarkdown/CustomFabricUIManager.java +2 -2
  9. package/android/src/main/java/com/expensify/livemarkdown/CustomMountingManager.java +4 -6
  10. package/android/src/main/java/com/expensify/livemarkdown/LiveMarkdownModule.java +12 -0
  11. package/android/src/main/java/com/expensify/livemarkdown/MarkdownTextInputDecoratorView.java +17 -3
  12. package/android/src/main/java/com/expensify/livemarkdown/MarkdownTextInputDecoratorViewManager.java +6 -0
  13. package/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java +21 -34
  14. package/android/src/main/new_arch/MarkdownCommitHook.cpp +7 -4
  15. package/android/src/main/new_arch/MarkdownCommitHook.h +2 -0
  16. package/android/src/newarch/LiveMarkdownModuleSpec.java +9 -0
  17. package/android/src/oldarch/LiveMarkdownModuleSpec.java +12 -0
  18. package/android/src/oldarch/MarkdownTextInputDecoratorViewManagerSpec.java +1 -0
  19. package/apple/LiveMarkdownModule.h +16 -0
  20. package/apple/{RCTLiveMarkdownModule.mm → LiveMarkdownModule.mm} +31 -17
  21. package/apple/MarkdownCommitHook.mm +2 -0
  22. package/apple/MarkdownTextInputDecoratorComponentView.mm +4 -0
  23. package/apple/MarkdownTextInputDecoratorView.h +2 -0
  24. package/apple/MarkdownTextInputDecoratorView.mm +13 -0
  25. package/apple/MarkdownTextInputDecoratorViewManager.mm +9 -0
  26. package/apple/RCTMarkdownUtils.h +1 -0
  27. package/apple/RCTMarkdownUtils.mm +43 -45
  28. package/cpp/MarkdownGlobal.cpp +45 -0
  29. package/cpp/MarkdownGlobal.h +24 -0
  30. package/cpp/RuntimeDecorator.cpp +43 -0
  31. package/cpp/RuntimeDecorator.h +13 -0
  32. package/lib/commonjs/MarkdownTextInput.js +43 -2
  33. package/lib/commonjs/MarkdownTextInput.js.map +1 -1
  34. package/lib/commonjs/MarkdownTextInput.web.js +20 -14
  35. package/lib/commonjs/MarkdownTextInput.web.js.map +1 -1
  36. package/lib/commonjs/MarkdownTextInputDecoratorViewNativeComponent.js.map +1 -1
  37. package/lib/commonjs/index.js +7 -0
  38. package/lib/commonjs/index.js.map +1 -1
  39. package/lib/commonjs/parseExpensiMark.js +263 -0
  40. package/lib/commonjs/parseExpensiMark.js.map +1 -0
  41. package/lib/commonjs/web/utils/parserUtils.js +2 -2
  42. package/lib/commonjs/web/utils/parserUtils.js.map +1 -1
  43. package/lib/module/MarkdownTextInput.js +43 -2
  44. package/lib/module/MarkdownTextInput.js.map +1 -1
  45. package/lib/module/MarkdownTextInput.web.js +20 -14
  46. package/lib/module/MarkdownTextInput.web.js.map +1 -1
  47. package/lib/module/MarkdownTextInputDecoratorViewNativeComponent.js.map +1 -1
  48. package/lib/module/index.js +1 -0
  49. package/lib/module/index.js.map +1 -1
  50. package/lib/module/parseExpensiMark.js +258 -0
  51. package/lib/module/parseExpensiMark.js.map +1 -0
  52. package/lib/module/web/utils/parserUtils.js +2 -2
  53. package/lib/module/web/utils/parserUtils.js.map +1 -1
  54. package/lib/typescript/src/MarkdownTextInput.d.ts +9 -1
  55. package/lib/typescript/src/MarkdownTextInput.d.ts.map +1 -1
  56. package/lib/typescript/src/MarkdownTextInput.web.d.ts +2 -1
  57. package/lib/typescript/src/MarkdownTextInput.web.d.ts.map +1 -1
  58. package/lib/typescript/src/MarkdownTextInputDecoratorViewNativeComponent.d.ts +2 -1
  59. package/lib/typescript/src/MarkdownTextInputDecoratorViewNativeComponent.d.ts.map +1 -1
  60. package/lib/typescript/src/index.d.ts +2 -0
  61. package/lib/typescript/src/index.d.ts.map +1 -1
  62. package/lib/typescript/src/parseExpensiMark.d.ts +4 -0
  63. package/lib/typescript/src/parseExpensiMark.d.ts.map +1 -0
  64. package/lib/typescript/src/web/utils/parserUtils.d.ts +1 -1
  65. package/lib/typescript/src/web/utils/parserUtils.d.ts.map +1 -1
  66. package/package.json +13 -10
  67. package/src/MarkdownTextInput.tsx +62 -2
  68. package/src/MarkdownTextInput.web.tsx +23 -15
  69. package/src/MarkdownTextInputDecoratorViewNativeComponent.ts +2 -1
  70. package/src/index.tsx +2 -0
  71. package/src/parseExpensiMark.ts +277 -0
  72. package/src/web/utils/parserUtils.ts +2 -1
  73. package/apple/RCTLiveMarkdownModule.h +0 -16
  74. package/lib/parser/react-native-live-markdown-parser.js +0 -29
  75. package/parser/react-native-live-markdown-parser.js +0 -29
package/README.md CHANGED
@@ -5,6 +5,7 @@
5
5
  - ⚛️ Drop-in replacement for `<TextInput>` component
6
6
  - ⌨️ Live synchronous formatting on every keystroke
7
7
  - ⚡ Fully native experience (selection, spellcheck, autocomplete)
8
+ - 🔧 Customizable logic
8
9
  - 🎨 Customizable styles
9
10
  - 🌐 Universal support (Android, iOS, web)
10
11
  - 🏗️ Supports New Architecture
@@ -14,15 +15,17 @@
14
15
  First, install the library from npm with the package manager of your choice:
15
16
 
16
17
  ```sh
17
- yarn add @expensify/react-native-live-markdown
18
- npm install @expensify/react-native-live-markdown --save
19
- npx expo install @expensify/react-native-live-markdown
18
+ yarn add @expensify/react-native-live-markdown react-native-reanimated expensify-common
19
+ npm install @expensify/react-native-live-markdown react-native-reanimated expensify-common --save
20
+ npx expo install @expensify/react-native-live-markdown react-native-reanimated expensify-common
20
21
  ```
21
22
 
23
+ React Native Live Markdown requires [react-native-reanimated](https://github.com/software-mansion/react-native-reanimated) 3.16.3 or newer and [expensify-common](https://github.com/Expensify/expensify-common) 2.0.106 or newer.
24
+
22
25
  Then, install the iOS dependencies with CocoaPods:
23
26
 
24
27
  ```sh
25
- cd ios && pod install
28
+ cd ios && bundler install && bundler exec pod install
26
29
  ```
27
30
 
28
31
  The library includes native code so you will need to re-build the native app.
@@ -33,7 +36,7 @@ The library includes native code so you will need to re-build the native app.
33
36
  ## Usage
34
37
 
35
38
  ```tsx
36
- import {MarkdownTextInput} from '@expensify/react-native-live-markdown';
39
+ import {MarkdownTextInput, parseExpensiMark} from '@expensify/react-native-live-markdown';
37
40
  import React from 'react';
38
41
 
39
42
  export default function App() {
@@ -43,6 +46,7 @@ export default function App() {
43
46
  <MarkdownTextInput
44
47
  value={text}
45
48
  onChangeText={setText}
49
+ parser={parseExpensiMark}
46
50
  />
47
51
  );
48
52
  }
@@ -118,6 +122,48 @@ The style object can be passed to multiple `MarkdownTextInput` components using
118
122
  > [!TIP]
119
123
  > We recommend to store the style object outside of a component body or memoize the style object with `React.useMemo`.
120
124
 
125
+ ## Parsing logic
126
+
127
+ `MarkdownTextInput` behavior can be customized via `parser` property. Parser is a function that accepts a plaintext string and returns an array of `MarkdownRange` objects:
128
+
129
+ ```ts
130
+ interface MarkdownRange {
131
+ type: MarkdownType;
132
+ start: number;
133
+ length: number;
134
+ depth?: number;
135
+ }
136
+ ```
137
+
138
+ Currently, only the following types are supported:
139
+
140
+ ```ts
141
+ type MarkdownType = 'bold' | 'italic' | 'strikethrough' | 'emoji' | 'mention-here' | 'mention-user' | 'mention-report' | 'link' | 'code' | 'pre' | 'blockquote' | 'h1' | 'syntax';
142
+ ```
143
+
144
+ Parser needs to be marked as a [worklet](https://docs.swmansion.com/react-native-reanimated/docs/guides/worklets/) because it's executed on the UI thread as the user types.
145
+
146
+ Here's a sample function that parses all substrings located between two asterisks as bold text:
147
+
148
+ ```ts
149
+ function parser(input: string) {
150
+ 'worklet';
151
+
152
+ const ranges = [];
153
+ const regexp = /\*(.*?)\*/g;
154
+ let match;
155
+ while ((match = regexp.exec(input)) !== null) {
156
+ ranges.push({start: match.index, length: 1, type: 'syntax'});
157
+ ranges.push({start: match.index + 1, length: match[1]!.length, type: 'bold'});
158
+ ranges.push({start: match.index + 1 + match[1]!.length, length: 1, type: 'syntax'});
159
+ }
160
+ return ranges;
161
+ }
162
+ ```
163
+
164
+ > [!TIP]
165
+ > We recommend to store the parser function outside of a component body or memoize the parser function with `React.useMemo`.
166
+
121
167
  ## Markdown flavors support
122
168
 
123
169
  Currently, `react-native-live-markdown` supports only [ExpensiMark](https://github.com/Expensify/expensify-common/blob/main/lib/ExpensiMark.ts) flavor. We are working on CommonMark support as well as possibility to use other Markdown parsers.
@@ -126,9 +172,10 @@ Currently, `react-native-live-markdown` supports only [ExpensiMark](https://gith
126
172
 
127
173
  `MarkdownTextInput` inherits all props of React Native's `TextInput` component as well as introduces the following properties:
128
174
 
129
- | Prop | Type | Default | Note |
130
- | --------------- | --------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
131
- | `markdownStyle` | `MarkdownStyle` | `undefined` | Adds custom styling to Markdown text. The provided value is merged with default style object. See [Styling](https://github.com/expensify/react-native-live-markdown/blob/main/README.md#styling) for more information. |
175
+ | Prop | Type | Default | Note |
176
+ | --------------- | ------------------------------------ | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
177
+ | `parser` | `(value: string) => MarkdownRange[]` | `undefined` | A function that parses the current value and returns an array of ranges. |
178
+ | `markdownStyle` | `MarkdownStyle` | `undefined` | Adds custom styling to Markdown text. The provided value is merged with default style object. See [Styling](https://github.com/expensify/react-native-live-markdown/blob/main/README.md#styling) for more information. |
132
179
 
133
180
  ## Compatibility
134
181
 
@@ -18,11 +18,9 @@ Pod::Spec.new do |s|
18
18
  s.platforms = { :ios => "11.0", :visionos => "1.0" }
19
19
  s.source = { :git => "https://github.com/expensify/react-native-live-markdown.git", :tag => "#{s.version}" }
20
20
 
21
- s.source_files = "apple/**/*.{h,m,mm}"
21
+ s.source_files = "apple/**/*.{h,m,mm}", "cpp/**/*.{h,cpp}"
22
22
 
23
- s.resources = "parser/react-native-live-markdown-parser.js"
24
-
25
- s.dependency "hermes-engine"
23
+ s.dependency "RNReanimated/worklets"
26
24
 
27
25
  s.xcconfig = {
28
26
  "OTHER_CFLAGS" => "$(inherited) -DREACT_NATIVE_MINOR_VERSION=#{react_native_minor_version}"
@@ -156,6 +156,9 @@ android {
156
156
  "**/libjsi.so",
157
157
  "**/libreactnativejni.so",
158
158
  "**/libreactnative.so",
159
+ "**/libreact_nativemodule_core.so",
160
+ "**/libruntimeexecutor.so",
161
+ "**/libworklets.so",
159
162
  "**/libreact_render*.so",
160
163
  "**/librrc_root.so",
161
164
  ]
@@ -171,6 +174,7 @@ repositories {
171
174
  dependencies {
172
175
  implementation "com.facebook.react:react-android" // version substituted by RNGP
173
176
  implementation "com.facebook.react:hermes-android" // version substituted by RNGP
177
+ implementation project(":react-native-reanimated")
174
178
  }
175
179
 
176
180
  if (isNewArchitectureEnabled()) {
@@ -180,12 +184,3 @@ if (isNewArchitectureEnabled()) {
180
184
  codegenJavaPackageName = "com.expensify.livemarkdown"
181
185
  }
182
186
  }
183
-
184
- task copyJS(type: Copy) {
185
- from '../parser/react-native-live-markdown-parser.js'
186
- into 'src/main/assets'
187
- }
188
-
189
- tasks.preBuild {
190
- dependsOn copyJS
191
- }
@@ -8,27 +8,35 @@ add_compile_options(-fvisibility=hidden -fexceptions -frtti)
8
8
 
9
9
  string(APPEND CMAKE_CXX_FLAGS " -DREACT_NATIVE_MINOR_VERSION=${REACT_NATIVE_MINOR_VERSION}")
10
10
 
11
- file(GLOB livemarkdown_SRC CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
11
+ set(CPP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../../cpp")
12
12
 
13
- add_library(${CMAKE_PROJECT_NAME} SHARED ${livemarkdown_SRC})
13
+ file(GLOB ANDROID_SRC CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
14
+ file(GLOB CPP_SRC CONFIGURE_DEPENDS "${CPP_DIR}/*.cpp")
14
15
 
15
- target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
16
+ add_library(${CMAKE_PROJECT_NAME} SHARED ${ANDROID_SRC} ${CPP_SRC})
17
+
18
+ target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CPP_DIR})
16
19
 
17
20
  find_package(fbjni REQUIRED CONFIG)
18
21
  find_package(ReactAndroid REQUIRED CONFIG)
19
- find_package(hermes-engine REQUIRED CONFIG)
22
+ find_package(react-native-reanimated REQUIRED CONFIG)
20
23
 
21
24
  target_link_libraries(
22
25
  ${CMAKE_PROJECT_NAME}
23
26
  fbjni::fbjni
24
- hermes-engine::libhermes
25
27
  ReactAndroid::jsi
28
+ react-native-reanimated::worklets
26
29
  )
27
30
 
28
31
  if (ReactAndroid_VERSION_MINOR GREATER_EQUAL 76)
29
32
  target_link_libraries(${CMAKE_PROJECT_NAME} ReactAndroid::reactnative)
30
33
  elseif (ReactAndroid_VERSION_MINOR GREATER_EQUAL 75)
31
- target_link_libraries(${CMAKE_PROJECT_NAME} ReactAndroid::reactnativejni)
34
+ target_link_libraries(
35
+ ${CMAKE_PROJECT_NAME}
36
+ ReactAndroid::react_nativemodule_core
37
+ ReactAndroid::reactnativejni
38
+ ReactAndroid::runtimeexecutor
39
+ )
32
40
  else ()
33
41
  message(FATAL_ERROR "react-native-live-markdown requires react-native 0.75 or newer.")
34
42
  endif ()
@@ -1,42 +1,31 @@
1
1
  #include "MarkdownUtils.h"
2
+ #include "MarkdownGlobal.h"
2
3
 
3
4
  #include <fbjni/fbjni.h>
4
- #include <hermes/hermes.h>
5
5
 
6
6
  using namespace facebook;
7
7
 
8
8
  namespace expensify {
9
9
  namespace livemarkdown {
10
- std::shared_ptr<jsi::Runtime> MarkdownUtils::runtime_;
11
-
12
- void MarkdownUtils::nativeInitializeRuntime(
13
- jni::alias_ref<jhybridobject> jThis,
14
- jni::alias_ref<jni::JString> code) {
15
- assert(runtime_ == nullptr && "Markdown runtime is already initialized");
16
- runtime_ = facebook::hermes::makeHermesRuntime();
17
- auto codeBuffer = std::make_shared<const jsi::StringBuffer>(code->toStdString());
18
- runtime_->evaluateJavaScript(codeBuffer, "nativeInitializeRuntime");
19
- }
20
-
21
10
  jni::local_ref<jni::JString> MarkdownUtils::nativeParseMarkdown(
22
11
  jni::alias_ref<jhybridobject> jThis,
23
- jni::alias_ref<jni::JString> input) {
24
- jsi::Runtime &rt = *runtime_;
25
- auto func = rt.global().getPropertyAsFunction(rt, "parseExpensiMarkToRanges");
26
- auto arg = input->toStdString();
27
- jsi::Value result;
28
- try {
29
- result = func.call(rt, arg);
30
- } catch (jsi::JSError e) {
31
- result = jsi::Array(rt, 0);
32
- }
33
- auto json = rt.global().getPropertyAsObject(rt, "JSON").getPropertyAsFunction(rt, "stringify").call(rt, result).asString(rt).utf8(rt);
12
+ jni::alias_ref<jni::JString> input,
13
+ int parserId) {
14
+ // This method is synchronized (see MarkdownUtils.java) so we don't need a mutex here.
15
+ const auto markdownRuntime = expensify::livemarkdown::getMarkdownRuntime();
16
+ jsi::Runtime &rt = markdownRuntime->getJSIRuntime();
17
+
18
+ const auto markdownWorklet = expensify::livemarkdown::getMarkdownWorklet(parserId);
19
+
20
+ const auto text = jsi::String::createFromUtf8(rt, input->toStdString());
21
+ const auto result = markdownRuntime->runGuarded(markdownWorklet, text);
22
+
23
+ const auto json = rt.global().getPropertyAsObject(rt, "JSON").getPropertyAsFunction(rt, "stringify").call(rt, result).asString(rt).utf8(rt);
34
24
  return jni::make_jstring(json);
35
25
  }
36
26
 
37
27
  void MarkdownUtils::registerNatives() {
38
28
  registerHybrid({
39
- makeNativeMethod("nativeInitializeRuntime", MarkdownUtils::nativeInitializeRuntime),
40
29
  makeNativeMethod("nativeParseMarkdown", MarkdownUtils::nativeParseMarkdown)});
41
30
  }
42
31
 
@@ -21,19 +21,14 @@ namespace livemarkdown {
21
21
  static constexpr auto kJavaDescriptor =
22
22
  "Lcom/expensify/livemarkdown/MarkdownUtils;";
23
23
 
24
- static void nativeInitializeRuntime(
25
- jni::alias_ref<jhybridobject> jThis,
26
- jni::alias_ref<jni::JString> code);
27
-
28
24
  static jni::local_ref<jni::JString> nativeParseMarkdown(
29
25
  jni::alias_ref<jhybridobject> jThis,
30
- jni::alias_ref<jni::JString> input);
26
+ jni::alias_ref<jni::JString> input,
27
+ int parserId);
31
28
 
32
29
  static void registerNatives();
33
30
 
34
31
  private:
35
- static std::shared_ptr<jsi::Runtime> runtime_;
36
-
37
32
  friend HybridBase;
38
33
  };
39
34
 
@@ -1,8 +1,14 @@
1
1
  #include <fbjni/fbjni.h>
2
2
 
3
3
  #include "MarkdownUtils.h"
4
+ #include "RuntimeDecorator.h"
4
5
 
5
6
  JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
6
7
  return facebook::jni::initialize(
7
8
  vm, [] { expensify::livemarkdown::MarkdownUtils::registerNatives(); });
8
9
  }
10
+
11
+ extern "C" JNIEXPORT void JNICALL Java_com_expensify_livemarkdown_LiveMarkdownModule_injectJSIBindings(JNIEnv *env, jobject thiz, jlong jsiRuntime) {
12
+ jsi::Runtime &rt = *reinterpret_cast<jsi::Runtime*>(jsiRuntime);
13
+ expensify::livemarkdown::injectJSIBindings(rt);
14
+ }
@@ -13,7 +13,7 @@ import java.lang.reflect.Field;
13
13
 
14
14
  public class CustomFabricUIManager {
15
15
 
16
- public static FabricUIManager create(FabricUIManager source, ReadableMap markdownProps) {
16
+ public static FabricUIManager create(FabricUIManager source, ReadableMap markdownProps, int parserId) {
17
17
  Class<? extends FabricUIManager> uiManagerClass = source.getClass();
18
18
 
19
19
  try {
@@ -27,7 +27,7 @@ public class CustomFabricUIManager {
27
27
 
28
28
  FabricUIManager customFabricUIManager = new FabricUIManager(reactContext, viewManagerRegistry, batchEventDispatchedListener);
29
29
 
30
- mountingManagerField.set(customFabricUIManager, new CustomMountingManager(viewManagerRegistry, mountItemExecutor, reactContext, markdownProps));
30
+ mountingManagerField.set(customFabricUIManager, new CustomMountingManager(viewManagerRegistry, mountItemExecutor, reactContext, markdownProps, parserId));
31
31
 
32
32
  return customFabricUIManager;
33
33
  } catch (NoSuchFieldException | IllegalAccessException e) {
@@ -38,14 +38,12 @@ public class CustomMountingManager extends MountingManager {
38
38
  @NonNull ViewManagerRegistry viewManagerRegistry,
39
39
  @NonNull MountItemExecutor mountItemExecutor,
40
40
  @NonNull Context context,
41
- @NonNull ReadableMap decoratorProps) {
41
+ @NonNull ReadableMap decoratorProps,
42
+ int parserId) {
42
43
  super(viewManagerRegistry, mountItemExecutor);
43
-
44
- AssetManager assetManager = context.getAssets();
45
- MarkdownUtils.maybeInitializeRuntime(assetManager);
46
-
47
- this.markdownUtils = new MarkdownUtils(assetManager);
44
+ this.markdownUtils = new MarkdownUtils((ReactContext) context);
48
45
  this.markdownUtils.setMarkdownStyle(new MarkdownStyle(decoratorProps, context));
46
+ this.markdownUtils.setParserId(parserId);
49
47
  }
50
48
 
51
49
  @Override
@@ -4,8 +4,15 @@ import com.facebook.react.bridge.ReactApplicationContext;
4
4
  import com.facebook.react.fabric.FabricUIManager;
5
5
  import com.facebook.react.uimanager.UIManagerHelper;
6
6
  import com.facebook.react.uimanager.common.UIManagerType;
7
+ import com.facebook.soloader.SoLoader;
8
+
9
+ import java.util.Objects;
7
10
 
8
11
  public class LiveMarkdownModule extends NativeLiveMarkdownModuleSpec {
12
+ static {
13
+ SoLoader.loadLibrary("livemarkdown");
14
+ }
15
+
9
16
  private NativeProxy mNativeProxy;
10
17
  public LiveMarkdownModule(ReactApplicationContext reactContext) {
11
18
  super(reactContext);
@@ -21,6 +28,11 @@ public class LiveMarkdownModule extends NativeLiveMarkdownModuleSpec {
21
28
  mNativeProxy.createCommitHook(uiManager);
22
29
  }
23
30
 
31
+ long jsiRuntime = Objects.requireNonNull(getReactApplicationContext().getJavaScriptContextHolder(), "[react-native-live-markdown] JavaScriptContextHolder is null").get();
32
+ injectJSIBindings(jsiRuntime);
33
+
24
34
  return true;
25
35
  }
36
+
37
+ private native void injectJSIBindings(long jsiRuntime);
26
38
  }
@@ -11,6 +11,7 @@ import android.view.View;
11
11
  import android.view.ViewGroup;
12
12
  import android.view.ViewParent;
13
13
 
14
+ import com.facebook.react.bridge.ReactContext;
14
15
  import com.facebook.react.views.textinput.ReactEditText;
15
16
 
16
17
  public class MarkdownTextInputDecoratorView extends View {
@@ -29,6 +30,8 @@ public class MarkdownTextInputDecoratorView extends View {
29
30
 
30
31
  private MarkdownStyle mMarkdownStyle;
31
32
 
33
+ private int mParserId;
34
+
32
35
  private MarkdownUtils mMarkdownUtils;
33
36
 
34
37
  private ReactEditText mReactEditText;
@@ -52,10 +55,9 @@ public class MarkdownTextInputDecoratorView extends View {
52
55
  }
53
56
 
54
57
  if (previousSibling instanceof ReactEditText) {
55
- AssetManager assetManager = getContext().getAssets();
56
- MarkdownUtils.maybeInitializeRuntime(assetManager);
57
- mMarkdownUtils = new MarkdownUtils(assetManager);
58
+ mMarkdownUtils = new MarkdownUtils((ReactContext) getContext());
58
59
  mMarkdownUtils.setMarkdownStyle(mMarkdownStyle);
60
+ mMarkdownUtils.setParserId(mParserId);
59
61
  mReactEditText = (ReactEditText) previousSibling;
60
62
  mTextWatcher = new MarkdownTextWatcher(mMarkdownUtils);
61
63
  mReactEditText.addTextChangedListener(mTextWatcher);
@@ -78,6 +80,18 @@ public class MarkdownTextInputDecoratorView extends View {
78
80
  if (mMarkdownUtils != null) {
79
81
  mMarkdownUtils.setMarkdownStyle(mMarkdownStyle);
80
82
  }
83
+ applyNewStyles();
84
+ }
85
+
86
+ protected void setParserId(int parserId) {
87
+ mParserId = parserId;
88
+ if (mMarkdownUtils != null) {
89
+ mMarkdownUtils.setParserId(mParserId);
90
+ }
91
+ applyNewStyles();
92
+ }
93
+
94
+ protected void applyNewStyles() {
81
95
  if (mReactEditText != null) {
82
96
  int selectionStart = mReactEditText.getSelectionStart();
83
97
  int selectionEnd = mReactEditText.getSelectionEnd();
@@ -28,4 +28,10 @@ public class MarkdownTextInputDecoratorViewManager extends MarkdownTextInputDeco
28
28
  MarkdownStyle markdownStyle = new MarkdownStyle(value, view.getContext());
29
29
  view.setMarkdownStyle(markdownStyle);
30
30
  }
31
+
32
+ @Override
33
+ @ReactProp(name = "parserId")
34
+ public void setParserId(@NonNull MarkdownTextInputDecoratorView view, int parserId) {
35
+ view.setParserId(parserId);
36
+ }
31
37
  }
@@ -7,6 +7,8 @@ import android.text.Spanned;
7
7
  import androidx.annotation.NonNull;
8
8
 
9
9
  import com.expensify.livemarkdown.spans.*;
10
+ import com.facebook.react.bridge.ReactContext;
11
+ import com.facebook.react.util.RNLog;
10
12
  import com.facebook.react.views.text.internal.span.CustomLineHeightSpan;
11
13
  import com.facebook.soloader.SoLoader;
12
14
 
@@ -14,8 +16,6 @@ import org.json.JSONArray;
14
16
  import org.json.JSONException;
15
17
  import org.json.JSONObject;
16
18
 
17
- import java.io.IOException;
18
- import java.io.InputStream;
19
19
  import java.util.Objects;
20
20
 
21
21
  public class MarkdownUtils {
@@ -23,49 +23,31 @@ public class MarkdownUtils {
23
23
  SoLoader.loadLibrary("livemarkdown");
24
24
  }
25
25
 
26
- private static boolean IS_RUNTIME_INITIALIZED = false;
26
+ private static synchronized native String nativeParseMarkdown(String input, int parserId);
27
27
 
28
- public static synchronized void maybeInitializeRuntime(AssetManager assetManager) {
29
- if (IS_RUNTIME_INITIALIZED) {
30
- return;
31
- }
32
- try {
33
- InputStream inputStream = assetManager.open("react-native-live-markdown-parser.js");
34
- byte[] buffer = new byte[inputStream.available()];
35
- inputStream.read(buffer);
36
- inputStream.close();
37
- String code = new String(buffer);
38
- nativeInitializeRuntime(code);
39
- IS_RUNTIME_INITIALIZED = true;
40
- } catch (IOException e) {
41
- throw new RuntimeException("Failed to initialize Markdown runtime");
42
- }
43
- }
44
-
45
- private static native void nativeInitializeRuntime(String code);
46
-
47
- private synchronized static String parseMarkdown(String input) {
48
- return nativeParseMarkdown(input);
49
- }
50
-
51
- private static native String nativeParseMarkdown(String input);
52
-
53
- public MarkdownUtils(@NonNull AssetManager assetManager) {
54
- mAssetManager = assetManager;
28
+ public MarkdownUtils(@NonNull ReactContext reactContext) {
29
+ mReactContext = reactContext;
30
+ mAssetManager = reactContext.getAssets();
55
31
  }
56
32
 
33
+ private final @NonNull ReactContext mReactContext;
57
34
  private final @NonNull AssetManager mAssetManager;
58
35
 
59
36
  private String mPrevInput;
60
-
61
37
  private String mPrevOutput;
38
+ private int mPrevParserId;
62
39
 
63
40
  private MarkdownStyle mMarkdownStyle;
41
+ private int mParserId;
64
42
 
65
43
  public void setMarkdownStyle(@NonNull MarkdownStyle markdownStyle) {
66
44
  mMarkdownStyle = markdownStyle;
67
45
  }
68
46
 
47
+ public void setParserId(int parserId) {
48
+ mParserId = parserId;
49
+ }
50
+
69
51
  public void applyMarkdownFormatting(SpannableStringBuilder ssb) {
70
52
  Objects.requireNonNull(mMarkdownStyle, "mMarkdownStyle is null");
71
53
 
@@ -73,12 +55,17 @@ public class MarkdownUtils {
73
55
 
74
56
  String input = ssb.toString();
75
57
  String output;
76
- if (input.equals(mPrevInput)) {
58
+ if (input.equals(mPrevInput) && mParserId == mPrevParserId) {
77
59
  output = mPrevOutput;
78
60
  } else {
79
- output = parseMarkdown(input);
61
+ try {
62
+ output = nativeParseMarkdown(input, mParserId);
63
+ } catch (Exception e) {
64
+ output = "[]";
65
+ }
80
66
  mPrevInput = input;
81
67
  mPrevOutput = output;
68
+ mPrevParserId = mParserId;
82
69
  }
83
70
 
84
71
  try {
@@ -96,7 +83,7 @@ public class MarkdownUtils {
96
83
  applyRange(ssb, type, start, end, depth);
97
84
  }
98
85
  } catch (JSONException e) {
99
- // Do nothing
86
+ RNLog.w(mReactContext, "[react-native-live-markdown] Incorrect schema of worklet parser output: " + e.getMessage());
100
87
  }
101
88
  }
102
89
 
@@ -135,7 +135,8 @@ RootShadowNode::Unshared MarkdownCommitHook::shadowTreeWillCommit(
135
135
 
136
136
  const auto currentDecoratorProps =
137
137
  nodes.decorator->getProps()->rawProps["markdownStyle"];
138
-
138
+ const auto currentParserId =
139
+ nodes.decorator->getProps()->rawProps["parserId"].asInt();
139
140
  // if it's the first time we encounter this particular input or the
140
141
  // markdown styles have changed (in which case we need to reset the
141
142
  // cpp cache, to which we don't have a direct access), create a new
@@ -143,13 +144,14 @@ RootShadowNode::Unshared MarkdownCommitHook::shadowTreeWillCommit(
143
144
  // for this particular input
144
145
  if (!textLayoutManagers_.contains(nodes.textInput->getTag()) ||
145
146
  previousDecoratorProps_[nodes.textInput->getTag()] !=
146
- currentDecoratorProps) {
147
+ currentDecoratorProps ||
148
+ previousParserId_[nodes.textInput->getTag()] != currentParserId) {
147
149
  static auto customUIManagerClass = jni::findClassStatic(
148
150
  "com/expensify/livemarkdown/CustomFabricUIManager");
149
151
  static auto createCustomUIManager =
150
152
  customUIManagerClass
151
153
  ->getStaticMethod<JFabricUIManager::javaobject(
152
- JFabricUIManager::javaobject, ReadableMap::javaobject)>(
154
+ JFabricUIManager::javaobject, ReadableMap::javaobject, int)>(
153
155
  "create");
154
156
 
155
157
  auto const decoratorPropsRNM =
@@ -160,7 +162,7 @@ RootShadowNode::Unshared MarkdownCommitHook::shadowTreeWillCommit(
160
162
 
161
163
  const auto customUIManager = jni::make_global(createCustomUIManager(
162
164
  customUIManagerClass, fabricUIManager_.get(),
163
- decoratorPropsRM.get()));
165
+ decoratorPropsRM.get(), currentParserId));
164
166
  const ContextContainer::Shared contextContainer =
165
167
  std::make_shared<ContextContainer const>();
166
168
  contextContainer->insert("FabricUIManager", customUIManager);
@@ -168,6 +170,7 @@ RootShadowNode::Unshared MarkdownCommitHook::shadowTreeWillCommit(
168
170
  std::make_shared<TextLayoutManager>(contextContainer);
169
171
  previousDecoratorProps_[nodes.textInput->getTag()] =
170
172
  currentDecoratorProps;
173
+ previousParserId_[nodes.textInput->getTag()] = currentParserId;
171
174
  }
172
175
 
173
176
  // we need to replace the TextLayoutManager every time to make sure
@@ -47,6 +47,8 @@ private:
47
47
  textLayoutManagers_;
48
48
  std::unordered_map<facebook::react::Tag, folly::dynamic>
49
49
  previousDecoratorProps_;
50
+ std::unordered_map<facebook::react::Tag, int>
51
+ previousParserId_;
50
52
  std::unordered_map<facebook::react::Tag, int64_t>
51
53
  previousEventCount_;
52
54
  };
@@ -0,0 +1,9 @@
1
+ package com.expensify.livemarkdown;
2
+
3
+ import com.facebook.react.bridge.ReactApplicationContext;
4
+
5
+ abstract class LiveMarkdownModuleSpec extends NativeLiveMarkdownModuleSpec {
6
+ LiveMarkdownModuleSpec(ReactApplicationContext context) {
7
+ super(context);
8
+ }
9
+ }
@@ -0,0 +1,12 @@
1
+ package com.expensify.livemarkdown;
2
+
3
+ import com.facebook.react.bridge.ReactApplicationContext;
4
+ import com.facebook.react.bridge.ReactContextBaseJavaModule;
5
+
6
+ abstract class LiveMarkdownModuleSpec extends ReactContextBaseJavaModule {
7
+ LiveMarkdownModuleSpec(ReactApplicationContext context) {
8
+ super(context);
9
+ }
10
+
11
+ public abstract boolean install();
12
+ }
@@ -7,4 +7,5 @@ import com.facebook.react.uimanager.SimpleViewManager;
7
7
 
8
8
  public abstract class MarkdownTextInputDecoratorViewManagerSpec<T extends View> extends SimpleViewManager<T> {
9
9
  public abstract void setMarkdownStyle(T view, ReadableMap value);
10
+ public abstract void setParserId(T view, int value);
10
11
  }
@@ -0,0 +1,16 @@
1
+ #ifdef RCT_NEW_ARCH_ENABLED
2
+ #import <RNLiveMarkdownSpec/RNLiveMarkdownSpec.h>
3
+ #else
4
+ #import <React/RCTBridgeModule.h>
5
+ #endif // RCT_NEW_ARCH_ENABLED
6
+
7
+ #import <React/RCTEventEmitter.h>
8
+
9
+ // Without inheriting after RCTEventEmitter we don't get access to bridge
10
+ @interface LiveMarkdownModule : RCTEventEmitter
11
+ #ifdef RCT_NEW_ARCH_ENABLED
12
+ <NativeLiveMarkdownModuleSpec>
13
+ #else
14
+ <RCTBridgeModule>
15
+ #endif // RCT_NEW_ARCH_ENABLED
16
+ @end