-
Notifications
You must be signed in to change notification settings - Fork 65
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into @Skalakid/fix-emoji-formatting
- Loading branch information
Showing
13 changed files
with
383 additions
and
283 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
116 changes: 116 additions & 0 deletions
116
android/src/main/java/com/expensify/livemarkdown/MarkdownFormatter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
114
android/src/main/java/com/expensify/livemarkdown/MarkdownParser.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.