Skip to content

Commit

Permalink
Merge branch 'main' into @Skalakid/fix-emoji-formatting
Browse files Browse the repository at this point in the history
  • Loading branch information
Skalakid committed Dec 6, 2024
2 parents 692de7c + 01be83f commit bb101a9
Show file tree
Hide file tree
Showing 13 changed files with 383 additions and 283 deletions.
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,116 @@
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 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(SpannableStringBuilder ssb, List<MarkdownRange> markdownRanges, @NonNull MarkdownStyle markdownStyle) {
Objects.requireNonNull(markdownStyle, "mMarkdownStyle is null");
removeSpans(ssb);
applyRanges(ssb, markdownRanges, markdownStyle);
}

private void removeSpans(SpannableStringBuilder ssb) {
// 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);
}
}

private void applyRanges(SpannableStringBuilder ssb, List<MarkdownRange> markdownRanges, @NonNull MarkdownStyle markdownStyle) {
for (MarkdownRange markdownRange : markdownRanges) {
applyRange(ssb, markdownRange, markdownStyle);
}
}

private void applyRange(SpannableStringBuilder ssb, MarkdownRange markdownRange, MarkdownStyle markdownStyle) {
String type = markdownRange.getType();
int start = markdownRange.getStart();
int end = start + markdownRange.getLength();
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(SpannableStringBuilder ssb, MarkdownSpan span, int start, int end) {
ssb.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
114 changes: 114 additions & 0 deletions android/src/main/java/com/expensify/livemarkdown/MarkdownParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
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 org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;
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(String text, int parserId);

private void splitRangesOnEmojis(List<MarkdownRange> markdownRanges, String type) {
List<MarkdownRange> emojiRanges = new ArrayList<>();
for (MarkdownRange range : markdownRanges) {
if (range.getType().equals("emoji")) {
emojiRanges.add(range);
}
}

int i = 0;
int j = 0;
while (i < markdownRanges.size() && j < emojiRanges.size()) {
MarkdownRange currentRange = markdownRanges.get(i);
MarkdownRange emojiRange = emojiRanges.get(j);

if (!currentRange.getType().equals(type) || currentRange.getEnd() < emojiRange.getStart()) {
i += 1;
continue;
} else if (emojiRange.getStart() >= currentRange.getStart() && emojiRange.getEnd() <= currentRange.getEnd()) {
// Split range
MarkdownRange startRange = new MarkdownRange(currentRange.getType(), currentRange.getStart(), emojiRange.getStart() - currentRange.getStart(), currentRange.getDepth());
MarkdownRange endRange = new MarkdownRange(currentRange.getType(), emojiRange.getEnd(), currentRange.getEnd() - emojiRange.getEnd(), currentRange.getDepth());

markdownRanges.add(i + 1, startRange);
markdownRanges.add(i + 2, endRange);
markdownRanges.remove(i);
i = i + 1;
}
j += 1;
}
}


private List<MarkdownRange> parseRanges(String rangesJSON, String innerText) {
List<MarkdownRange> markdownRanges = new ArrayList<>();
try {
JSONArray ranges = new JSONArray(rangesJSON);
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);

MarkdownRange markdownRange = new MarkdownRange(type, start, length, depth);
if (markdownRange.getLength() == 0 || markdownRange.getEnd() > innerText.length()) {
continue;
}
markdownRanges.add(markdownRange);
}
} catch (JSONException e) {
return Collections.emptyList();
}
splitRangesOnEmojis(markdownRanges, "italic");
splitRangesOnEmojis(markdownRanges, "strikethrough");
return markdownRanges;
}

public synchronized List<MarkdownRange> 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<MarkdownRange> markdownRanges = parseRanges(json, text);

mPrevText = text;
mPrevParserId = parserId;
mPrevMarkdownRanges = markdownRanges;
return mPrevMarkdownRanges;
}
}
Loading

0 comments on commit bb101a9

Please sign in to comment.