Skip to content

Commit

Permalink
Merge branch 'main' into @jpiasecki/bump-to-76
Browse files Browse the repository at this point in the history
  • Loading branch information
tomekzaw committed Jan 9, 2025
2 parents 7a2afa3 + 149b3ee commit 575e68f
Show file tree
Hide file tree
Showing 41 changed files with 1,211 additions and 582 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ npm install @expensify/react-native-live-markdown react-native-reanimated expens
npx expo install @expensify/react-native-live-markdown react-native-reanimated expensify-common
```

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.
React Native Live Markdown requires [react-native-reanimated](https://github.com/software-mansion/react-native-reanimated) 3.16.4 or newer and [expensify-common](https://github.com/Expensify/expensify-common) 2.0.108 or newer.

Then, install the iOS dependencies with CocoaPods:

Expand Down
11 changes: 10 additions & 1 deletion RNLiveMarkdown.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ react_native_node_modules_dir = ENV['REACT_NATIVE_NODE_MODULES_DIR'] || File.joi
react_native_json = JSON.parse(File.read(File.join(react_native_node_modules_dir, 'react-native/package.json')))
react_native_minor_version = react_native_json['version'].split('.')[1].to_i

pods_root = Pod::Config.instance.project_pods_root
react_native_reanimated_node_modules_dir = ENV['REACT_NATIVE_REANIMATED_NODE_MODULES_DIR'] || File.dirname(`cd "#{Pod::Config.instance.installation_root.to_s}" && node --print "require.resolve('react-native-reanimated/package.json')"`)
react_native_reanimated_node_modules_dir_from_pods_root = Pathname.new(react_native_reanimated_node_modules_dir).relative_path_from(pods_root).to_s

package = JSON.parse(File.read(File.join(__dir__, "package.json")))
folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'

Expand All @@ -23,7 +27,11 @@ Pod::Spec.new do |s|
s.dependency "RNReanimated/worklets"

s.xcconfig = {
"OTHER_CFLAGS" => "$(inherited) -DREACT_NATIVE_MINOR_VERSION=#{react_native_minor_version}"
"OTHER_CFLAGS" => "$(inherited) -DREACT_NATIVE_MINOR_VERSION=#{react_native_minor_version}",
"HEADER_SEARCH_PATHS" => [
"\"$(PODS_ROOT)/#{react_native_reanimated_node_modules_dir_from_pods_root}/apple\"",
"\"$(PODS_ROOT)/#{react_native_reanimated_node_modules_dir_from_pods_root}/Common/cpp\"",
].join(' '),
}

install_modules_dependencies(s)
Expand All @@ -33,6 +41,7 @@ Pod::Spec.new do |s|
"react/renderer/textlayoutmanager/platform/ios",
"react/renderer/components/textinput/platform/ios",
])
add_dependency(s, "React-rendererconsistency")
end

if ENV['RCT_NEW_ARCH_ENABLED'] == '1'
Expand Down
35 changes: 35 additions & 0 deletions android/src/main/cpp/MarkdownParser.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#include "MarkdownParser.h"
#include "MarkdownGlobal.h"

#include <fbjni/fbjni.h>

using namespace facebook;

namespace expensify {
namespace livemarkdown {
jni::local_ref<jni::JString> MarkdownParser::nativeParse(
jni::alias_ref<jhybridobject> jThis,
jni::alias_ref<jni::JString> 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<std::mutex>(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
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@ using namespace facebook;
namespace expensify {
namespace livemarkdown {

class MarkdownUtils : public jni::HybridClass<MarkdownUtils>,
class MarkdownParser : public jni::HybridClass<MarkdownParser>,
public jsi::HostObject {
public:
static constexpr auto kJavaDescriptor =
"Lcom/expensify/livemarkdown/MarkdownUtils;";
"Lcom/expensify/livemarkdown/MarkdownParser;";

static jni::local_ref<jni::JString> nativeParseMarkdown(
static jni::local_ref<jni::JString> nativeParse(
jni::alias_ref<jhybridobject> jThis,
jni::alias_ref<jni::JString> input,
int parserId);
jni::alias_ref<jni::JString> text,
const int parserId);

static void registerNatives();

Expand Down
33 changes: 0 additions & 33 deletions android/src/main/cpp/MarkdownUtils.cpp

This file was deleted.

4 changes: 2 additions & 2 deletions android/src/main/cpp/OnLoad.cpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
#include <fbjni/fbjni.h>

#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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package com.expensify.livemarkdown;

import android.content.res.AssetManager;
import android.text.SpannableStringBuilder;
import android.text.Spanned;

import androidx.annotation.NonNull;

import com.expensify.livemarkdown.spans.*;
import com.facebook.react.views.text.internal.span.CustomLineHeightSpan;
import com.facebook.systrace.Systrace;

import java.util.List;
import java.util.Objects;

public class MarkdownFormatter {
private final @NonNull AssetManager mAssetManager;

public MarkdownFormatter(@NonNull AssetManager assetManager) {
mAssetManager = assetManager;
}

public void format(@NonNull SpannableStringBuilder ssb, @NonNull List<MarkdownRange> markdownRanges, @NonNull MarkdownStyle markdownStyle) {
try {
Systrace.beginSection(0, "format");
Objects.requireNonNull(markdownStyle, "mMarkdownStyle is null");
removeSpans(ssb);
applyRanges(ssb, markdownRanges, markdownStyle);
} finally {
Systrace.endSection(0);
}
}

private void removeSpans(@NonNull SpannableStringBuilder ssb) {
try {
Systrace.beginSection(0, "removeSpans");
// We shouldn't use `removeSpans()` because it also removes SpellcheckSpan, SuggestionSpan etc.
MarkdownSpan[] spans = ssb.getSpans(0, ssb.length(), MarkdownSpan.class);
for (MarkdownSpan span : spans) {
ssb.removeSpan(span);
}
} finally {
Systrace.endSection(0);
}
}

private void applyRanges(@NonNull SpannableStringBuilder ssb, @NonNull List<MarkdownRange> markdownRanges, @NonNull MarkdownStyle markdownStyle) {
try {
Systrace.beginSection(0, "applyRanges");
for (MarkdownRange markdownRange : markdownRanges) {
applyRange(ssb, markdownRange, markdownStyle);
}
} finally {
Systrace.endSection(0);
}
}

private void applyRange(@NonNull SpannableStringBuilder ssb, @NonNull MarkdownRange markdownRange, @NonNull MarkdownStyle markdownStyle) {
String type = markdownRange.getType();
int start = markdownRange.getStart();
int end = markdownRange.getEnd();
switch (type) {
case "bold":
setSpan(ssb, new MarkdownBoldSpan(), start, end);
break;
case "italic":
setSpan(ssb, new MarkdownItalicSpan(), start, end);
break;
case "strikethrough":
setSpan(ssb, new MarkdownStrikethroughSpan(), start, end);
break;
case "emoji":
setSpan(ssb, new MarkdownEmojiSpan(markdownStyle.getEmojiFontSize()), start, end);
break;
case "mention-here":
setSpan(ssb, new MarkdownForegroundColorSpan(markdownStyle.getMentionHereColor()), start, end);
setSpan(ssb, new MarkdownBackgroundColorSpan(markdownStyle.getMentionHereBackgroundColor()), start, end);
break;
case "mention-user":
// TODO: change mention color when it mentions current user
setSpan(ssb, new MarkdownForegroundColorSpan(markdownStyle.getMentionUserColor()), start, end);
setSpan(ssb, new MarkdownBackgroundColorSpan(markdownStyle.getMentionUserBackgroundColor()), start, end);
break;
case "mention-report":
setSpan(ssb, new MarkdownForegroundColorSpan(markdownStyle.getMentionReportColor()), start, end);
setSpan(ssb, new MarkdownBackgroundColorSpan(markdownStyle.getMentionReportBackgroundColor()), start, end);
break;
case "syntax":
setSpan(ssb, new MarkdownForegroundColorSpan(markdownStyle.getSyntaxColor()), start, end);
break;
case "link":
setSpan(ssb, new MarkdownUnderlineSpan(), start, end);
setSpan(ssb, new MarkdownForegroundColorSpan(markdownStyle.getLinkColor()), start, end);
break;
case "code":
setSpan(ssb, new MarkdownFontFamilySpan(markdownStyle.getCodeFontFamily(), mAssetManager), start, end);
setSpan(ssb, new MarkdownFontSizeSpan(markdownStyle.getCodeFontSize()), start, end);
setSpan(ssb, new MarkdownForegroundColorSpan(markdownStyle.getCodeColor()), start, end);
setSpan(ssb, new MarkdownBackgroundColorSpan(markdownStyle.getCodeBackgroundColor()), start, end);
break;
case "pre":
setSpan(ssb, new MarkdownFontFamilySpan(markdownStyle.getPreFontFamily(), mAssetManager), start, end);
setSpan(ssb, new MarkdownFontSizeSpan(markdownStyle.getPreFontSize()), start, end);
setSpan(ssb, new MarkdownForegroundColorSpan(markdownStyle.getPreColor()), start, end);
setSpan(ssb, new MarkdownBackgroundColorSpan(markdownStyle.getPreBackgroundColor()), start, end);
break;
case "h1":
setSpan(ssb, new MarkdownBoldSpan(), start, end);
CustomLineHeightSpan[] spans = ssb.getSpans(0, ssb.length(), CustomLineHeightSpan.class);
if (spans.length >= 1) {
int lineHeight = spans[0].getLineHeight();
setSpan(ssb, new MarkdownLineHeightSpan(lineHeight * 1.5f), start, end);
}
// NOTE: size span must be set after line height span to avoid height jumps
setSpan(ssb, new MarkdownFontSizeSpan(markdownStyle.getH1FontSize()), start, end);
break;
case "blockquote":
MarkdownBlockquoteSpan span = new MarkdownBlockquoteSpan(
markdownStyle.getBlockquoteBorderColor(),
markdownStyle.getBlockquoteBorderWidth(),
markdownStyle.getBlockquoteMarginLeft(),
markdownStyle.getBlockquotePaddingLeft(),
markdownRange.getDepth());
setSpan(ssb, span, start, end);
break;
}
}

private void setSpan(@NonNull SpannableStringBuilder ssb, @NonNull MarkdownSpan span, int start, int end) {
ssb.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.expensify.livemarkdown;

import androidx.annotation.NonNull;

import com.facebook.react.bridge.ReactContext;
import com.facebook.react.util.RNLog;
import com.facebook.soloader.SoLoader;
import com.facebook.systrace.Systrace;

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 {
static {
SoLoader.loadLibrary("livemarkdown");
}

private final @NonNull ReactContext mReactContext;
private String mPrevText;
private int mPrevParserId;
private List<MarkdownRange> mPrevMarkdownRanges;

public MarkdownParser(@NonNull ReactContext reactContext) {
mReactContext = reactContext;
}

private native String nativeParse(@NonNull String text, int parserId);

public synchronized List<MarkdownRange> parse(@NonNull String text, int parserId) {
try {
Systrace.beginSection(0, "parse");

if (text.equals(mPrevText) && parserId == mPrevParserId) {
return mPrevMarkdownRanges;
}

String json;
try {
Systrace.beginSection(0, "nativeParse");
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;
} finally {
Systrace.endSection(0);
}

List<MarkdownRange> markdownRanges = new LinkedList<>();
try {
Systrace.beginSection(0, "markdownRanges");
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;
} finally {
Systrace.endSection(0);
}

mPrevText = text;
mPrevParserId = parserId;
mPrevMarkdownRanges = markdownRanges;
return mPrevMarkdownRanges;
} finally {
Systrace.endSection(0);
}
}
}
Loading

0 comments on commit 575e68f

Please sign in to comment.