diff --git a/android/src/main/cpp/MarkdownParser.cpp b/android/src/main/cpp/MarkdownParser.cpp new file mode 100644 index 00000000..b645eb48 --- /dev/null +++ b/android/src/main/cpp/MarkdownParser.cpp @@ -0,0 +1,35 @@ +#include "MarkdownParser.h" +#include "MarkdownGlobal.h" + +#include + +using namespace facebook; + +namespace expensify { +namespace livemarkdown { + jni::local_ref MarkdownParser::nativeParse( + jni::alias_ref jThis, + jni::alias_ref text, + const int parserId) { + static std::mutex workletRuntimeMutex; // this needs to be global since the worklet runtime is also global + const auto lock = std::lock_guard(workletRuntimeMutex); + + const auto markdownRuntime = expensify::livemarkdown::getMarkdownRuntime(); + jsi::Runtime &rt = markdownRuntime->getJSIRuntime(); + + const auto markdownWorklet = expensify::livemarkdown::getMarkdownWorklet(parserId); + + const auto input = jsi::String::createFromUtf8(rt, text->toStdString()); + const auto output = markdownRuntime->runGuarded(markdownWorklet, input); + + const auto json = rt.global().getPropertyAsObject(rt, "JSON").getPropertyAsFunction(rt, "stringify").call(rt, output).asString(rt).utf8(rt); + return jni::make_jstring(json); + } + + void MarkdownParser::registerNatives() { + registerHybrid({ + makeNativeMethod("nativeParse", MarkdownParser::nativeParse)}); + } + +} // namespace livemarkdown +} // namespace expensify diff --git a/android/src/main/cpp/MarkdownUtils.h b/android/src/main/cpp/MarkdownParser.h similarity index 71% rename from android/src/main/cpp/MarkdownUtils.h rename to android/src/main/cpp/MarkdownParser.h index 4a509b87..fc3313b5 100644 --- a/android/src/main/cpp/MarkdownUtils.h +++ b/android/src/main/cpp/MarkdownParser.h @@ -15,16 +15,16 @@ using namespace facebook; namespace expensify { namespace livemarkdown { - class MarkdownUtils : public jni::HybridClass, + class MarkdownParser : public jni::HybridClass, public jsi::HostObject { public: static constexpr auto kJavaDescriptor = - "Lcom/expensify/livemarkdown/MarkdownUtils;"; + "Lcom/expensify/livemarkdown/MarkdownParser;"; - static jni::local_ref nativeParseMarkdown( + static jni::local_ref nativeParse( jni::alias_ref jThis, - jni::alias_ref input, - int parserId); + jni::alias_ref text, + const int parserId); static void registerNatives(); diff --git a/android/src/main/cpp/MarkdownUtils.cpp b/android/src/main/cpp/MarkdownUtils.cpp deleted file mode 100644 index 9621e46f..00000000 --- a/android/src/main/cpp/MarkdownUtils.cpp +++ /dev/null @@ -1,33 +0,0 @@ -#include "MarkdownUtils.h" -#include "MarkdownGlobal.h" - -#include - -using namespace facebook; - -namespace expensify { -namespace livemarkdown { - jni::local_ref MarkdownUtils::nativeParseMarkdown( - jni::alias_ref jThis, - jni::alias_ref input, - int parserId) { - // This method is synchronized (see MarkdownUtils.java) so we don't need a mutex here. - const auto markdownRuntime = expensify::livemarkdown::getMarkdownRuntime(); - jsi::Runtime &rt = markdownRuntime->getJSIRuntime(); - - const auto markdownWorklet = expensify::livemarkdown::getMarkdownWorklet(parserId); - - const auto text = jsi::String::createFromUtf8(rt, input->toStdString()); - const auto result = markdownRuntime->runGuarded(markdownWorklet, text); - - const auto json = rt.global().getPropertyAsObject(rt, "JSON").getPropertyAsFunction(rt, "stringify").call(rt, result).asString(rt).utf8(rt); - return jni::make_jstring(json); - } - - void MarkdownUtils::registerNatives() { - registerHybrid({ - makeNativeMethod("nativeParseMarkdown", MarkdownUtils::nativeParseMarkdown)}); - } - -} // namespace livemarkdown -} // namespace expensify diff --git a/android/src/main/cpp/OnLoad.cpp b/android/src/main/cpp/OnLoad.cpp index 47086340..7daa4a33 100644 --- a/android/src/main/cpp/OnLoad.cpp +++ b/android/src/main/cpp/OnLoad.cpp @@ -1,11 +1,11 @@ #include -#include "MarkdownUtils.h" +#include "MarkdownParser.h" #include "RuntimeDecorator.h" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { return facebook::jni::initialize( - vm, [] { expensify::livemarkdown::MarkdownUtils::registerNatives(); }); + vm, [] { expensify::livemarkdown::MarkdownParser::registerNatives(); }); } extern "C" JNIEXPORT void JNICALL Java_com_expensify_livemarkdown_LiveMarkdownModule_injectJSIBindings(JNIEnv *env, jobject thiz, jlong jsiRuntime) { diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownParser.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownParser.java new file mode 100644 index 00000000..17bd0e71 --- /dev/null +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownParser.java @@ -0,0 +1,71 @@ +package com.expensify.livemarkdown; + +import androidx.annotation.NonNull; + +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.util.RNLog; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +public class MarkdownParser { + private final @NonNull ReactContext mReactContext; + private String mPrevText; + private int mPrevParserId; + private List mPrevMarkdownRanges; + + public MarkdownParser(@NonNull ReactContext reactContext) { + mReactContext = reactContext; + } + + private native String nativeParse(String text, int parserId); + + public synchronized List parse(String text, int parserId) { + if (text.equals(mPrevText) && parserId == mPrevParserId) { + return mPrevMarkdownRanges; + } + + String json; + try { + json = nativeParse(text, parserId); + } catch (Exception e) { + // Skip formatting, runGuarded will show the error in LogBox + mPrevText = text; + mPrevParserId = parserId; + mPrevMarkdownRanges = Collections.emptyList(); + return mPrevMarkdownRanges; + } + + List markdownRanges = new LinkedList<>(); + try { + JSONArray ranges = new JSONArray(json); + for (int i = 0; i < ranges.length(); i++) { + JSONObject range = ranges.getJSONObject(i); + String type = range.getString("type"); + int start = range.getInt("start"); + int length = range.getInt("length"); + int depth = range.optInt("depth", 1); + if (length == 0 || start + length > text.length()) { + continue; + } + markdownRanges.add(new MarkdownRange(type, start, length, depth)); + } + } catch (JSONException e) { + RNLog.w(mReactContext, "[react-native-live-markdown] Incorrect schema of worklet parser output: " + e.getMessage()); + mPrevText = text; + mPrevParserId = parserId; + mPrevMarkdownRanges = Collections.emptyList(); + return mPrevMarkdownRanges; + } + + mPrevText = text; + mPrevParserId = parserId; + mPrevMarkdownRanges = markdownRanges; + return mPrevMarkdownRanges; + } +} diff --git a/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java b/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java index 3451fdb4..abb213d0 100644 --- a/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java +++ b/android/src/main/java/com/expensify/livemarkdown/MarkdownUtils.java @@ -8,15 +8,9 @@ import com.expensify.livemarkdown.spans.*; import com.facebook.react.bridge.ReactContext; -import com.facebook.react.util.RNLog; import com.facebook.react.views.text.internal.span.CustomLineHeightSpan; import com.facebook.soloader.SoLoader; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.LinkedList; import java.util.List; import java.util.Objects; @@ -25,19 +19,13 @@ public class MarkdownUtils { SoLoader.loadLibrary("livemarkdown"); } - private static synchronized native String nativeParseMarkdown(String input, int parserId); - public MarkdownUtils(@NonNull ReactContext reactContext) { - mReactContext = reactContext; mAssetManager = reactContext.getAssets(); + mMarkdownParser = new MarkdownParser(reactContext); } - private final @NonNull ReactContext mReactContext; private final @NonNull AssetManager mAssetManager; - - private String mPrevInput; - private String mPrevOutput; - private int mPrevParserId; + private final @NonNull MarkdownParser mMarkdownParser; private MarkdownStyle mMarkdownStyle; private int mParserId; @@ -55,39 +43,8 @@ public void applyMarkdownFormatting(SpannableStringBuilder ssb) { removeSpans(ssb); - String input = ssb.toString(); - String output; - if (input.equals(mPrevInput) && mParserId == mPrevParserId) { - output = mPrevOutput; - } else { - try { - output = nativeParseMarkdown(input, mParserId); - } catch (Exception e) { - output = "[]"; - } - mPrevInput = input; - mPrevOutput = output; - mPrevParserId = mParserId; - } - - List markdownRanges = new LinkedList<>(); - try { - JSONArray ranges = new JSONArray(output); - for (int i = 0; i < ranges.length(); i++) { - JSONObject range = ranges.getJSONObject(i); - String type = range.getString("type"); - int start = range.getInt("start"); - int length = range.getInt("length"); - int depth = range.optInt("depth", 1); - int end = start + length; - if (length == 0 || end > input.length()) { - continue; - } - markdownRanges.add(new MarkdownRange(type, start, length, depth)); - } - } catch (JSONException e) { - RNLog.w(mReactContext, "[react-native-live-markdown] Incorrect schema of worklet parser output: " + e.getMessage()); - } + String text = ssb.toString(); + List markdownRanges = mMarkdownParser.parse(text, mParserId); for (MarkdownRange markdownRange : markdownRanges) { applyRange(ssb, markdownRange);