@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.
- package/README.md +55 -8
- package/RNLiveMarkdown.podspec +2 -4
- package/android/build.gradle +4 -9
- package/android/src/main/cpp/CMakeLists.txt +14 -6
- package/android/src/main/cpp/MarkdownUtils.cpp +13 -24
- package/android/src/main/cpp/MarkdownUtils.h +2 -7
- package/android/src/main/cpp/OnLoad.cpp +6 -0
- package/android/src/main/java/com/expensify/livemarkdown/CustomFabricUIManager.java +2 -2
- package/android/src/main/java/com/expensify/livemarkdown/CustomMountingManager.java +4 -6
- package/android/src/main/java/com/expensify/livemarkdown/LiveMarkdownModule.java +12 -0
- package/android/src/main/java/com/expensify/livemarkdown/MarkdownTextInputDecoratorView.java +17 -3
- package/android/src/main/java/com/expensify/livemarkdown/MarkdownTextInputDecoratorViewManager.java +6 -0
- package/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java +21 -34
- package/android/src/main/new_arch/MarkdownCommitHook.cpp +7 -4
- package/android/src/main/new_arch/MarkdownCommitHook.h +2 -0
- package/android/src/newarch/LiveMarkdownModuleSpec.java +9 -0
- package/android/src/oldarch/LiveMarkdownModuleSpec.java +12 -0
- package/android/src/oldarch/MarkdownTextInputDecoratorViewManagerSpec.java +1 -0
- package/apple/LiveMarkdownModule.h +16 -0
- package/apple/{RCTLiveMarkdownModule.mm → LiveMarkdownModule.mm} +31 -17
- package/apple/MarkdownCommitHook.mm +2 -0
- package/apple/MarkdownTextInputDecoratorComponentView.mm +4 -0
- package/apple/MarkdownTextInputDecoratorView.h +2 -0
- package/apple/MarkdownTextInputDecoratorView.mm +13 -0
- package/apple/MarkdownTextInputDecoratorViewManager.mm +9 -0
- package/apple/RCTMarkdownUtils.h +1 -0
- package/apple/RCTMarkdownUtils.mm +43 -45
- package/cpp/MarkdownGlobal.cpp +45 -0
- package/cpp/MarkdownGlobal.h +24 -0
- package/cpp/RuntimeDecorator.cpp +43 -0
- package/cpp/RuntimeDecorator.h +13 -0
- package/lib/commonjs/MarkdownTextInput.js +43 -2
- package/lib/commonjs/MarkdownTextInput.js.map +1 -1
- package/lib/commonjs/MarkdownTextInput.web.js +20 -14
- package/lib/commonjs/MarkdownTextInput.web.js.map +1 -1
- package/lib/commonjs/MarkdownTextInputDecoratorViewNativeComponent.js.map +1 -1
- package/lib/commonjs/index.js +7 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/parseExpensiMark.js +263 -0
- package/lib/commonjs/parseExpensiMark.js.map +1 -0
- package/lib/commonjs/web/utils/parserUtils.js +2 -2
- package/lib/commonjs/web/utils/parserUtils.js.map +1 -1
- package/lib/module/MarkdownTextInput.js +43 -2
- package/lib/module/MarkdownTextInput.js.map +1 -1
- package/lib/module/MarkdownTextInput.web.js +20 -14
- package/lib/module/MarkdownTextInput.web.js.map +1 -1
- package/lib/module/MarkdownTextInputDecoratorViewNativeComponent.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/parseExpensiMark.js +258 -0
- package/lib/module/parseExpensiMark.js.map +1 -0
- package/lib/module/web/utils/parserUtils.js +2 -2
- package/lib/module/web/utils/parserUtils.js.map +1 -1
- package/lib/typescript/src/MarkdownTextInput.d.ts +9 -1
- package/lib/typescript/src/MarkdownTextInput.d.ts.map +1 -1
- package/lib/typescript/src/MarkdownTextInput.web.d.ts +2 -1
- package/lib/typescript/src/MarkdownTextInput.web.d.ts.map +1 -1
- package/lib/typescript/src/MarkdownTextInputDecoratorViewNativeComponent.d.ts +2 -1
- package/lib/typescript/src/MarkdownTextInputDecoratorViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/parseExpensiMark.d.ts +4 -0
- package/lib/typescript/src/parseExpensiMark.d.ts.map +1 -0
- package/lib/typescript/src/web/utils/parserUtils.d.ts +1 -1
- package/lib/typescript/src/web/utils/parserUtils.d.ts.map +1 -1
- package/package.json +13 -10
- package/src/MarkdownTextInput.tsx +62 -2
- package/src/MarkdownTextInput.web.tsx +23 -15
- package/src/MarkdownTextInputDecoratorViewNativeComponent.ts +2 -1
- package/src/index.tsx +2 -0
- package/src/parseExpensiMark.ts +277 -0
- package/src/web/utils/parserUtils.ts +2 -1
- package/apple/RCTLiveMarkdownModule.h +0 -16
- package/lib/parser/react-native-live-markdown-parser.js +0 -29
- 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
|
|
130
|
-
| --------------- |
|
|
131
|
-
| `
|
|
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
|
|
package/RNLiveMarkdown.podspec
CHANGED
|
@@ -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.
|
|
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}"
|
package/android/build.gradle
CHANGED
|
@@ -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
|
-
|
|
11
|
+
set(CPP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../../cpp")
|
|
12
12
|
|
|
13
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
auto
|
|
27
|
-
jsi::
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
}
|
package/android/src/main/java/com/expensify/livemarkdown/MarkdownTextInputDecoratorView.java
CHANGED
|
@@ -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
|
-
|
|
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();
|
package/android/src/main/java/com/expensify/livemarkdown/MarkdownTextInputDecoratorViewManager.java
CHANGED
|
@@ -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
|
|
26
|
+
private static synchronized native String nativeParseMarkdown(String input, int parserId);
|
|
27
27
|
|
|
28
|
-
public
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|