Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent italic or strikethrough emojis on Android #534

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ dependencies {
implementation "com.facebook.react:react-android" // version substituted by RNGP
implementation "com.facebook.react:hermes-android" // version substituted by RNGP
implementation project(":react-native-reanimated")

testImplementation "junit:junit:4.13.2"
}

if (isNewArchitectureEnabled()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.expensify.livemarkdown;

import static com.expensify.livemarkdown.RangeSplitter.splitRangesOnEmojis;

import androidx.annotation.NonNull;

import com.facebook.react.bridge.ReactContext;
Expand All @@ -11,6 +13,7 @@
import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
Expand All @@ -31,6 +34,30 @@ public MarkdownParser(@NonNull ReactContext reactContext) {

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

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();
}
markdownRanges = splitRangesOnEmojis(markdownRanges, "italic");
markdownRanges = splitRangesOnEmojis(markdownRanges, "strikethrough");
return markdownRanges;
}
public synchronized List<MarkdownRange> parse(@NonNull String text, int parserId) {
try {
Systrace.beginSection(0, "parse");
Expand All @@ -53,30 +80,8 @@ public synchronized List<MarkdownRange> parse(@NonNull String text, int parserId
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);
}
List<MarkdownRange> markdownRanges = parseRanges(json, text);
Systrace.endSection(0);

mPrevText = text;
mPrevParserId = parserId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,32 @@ public int getLength() {
public int getDepth() {
return mDepth;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}

if (o instanceof MarkdownRange other) {
return this.mType.equals(other.mType)
&& this.mStart == other.mStart
&& this.mEnd == other.mEnd
&& this.mLength == other.mLength
&& this.mDepth == other.mDepth;
}
return false;
}

@NonNull
@Override
public String toString() {
return "MarkdownRange{" +
"type='" + mType + "'" +
", start=" + mStart +
", end=" + mEnd +
", length=" + mLength +
", depth=" + mDepth +
"}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.expensify.livemarkdown;

import androidx.annotation.NonNull;

import com.facebook.systrace.Systrace;

import java.util.ArrayList;
import java.util.List;

public class RangeSplitter {
public static ArrayList<MarkdownRange> splitRangesOnEmojis(@NonNull List<MarkdownRange> markdownRanges, @NonNull String type) {
ArrayList<MarkdownRange> emojiRanges = new ArrayList<>();
ArrayList<MarkdownRange> oldRanges = new ArrayList<>(markdownRanges);
ArrayList<MarkdownRange> newRanges = new ArrayList<>();

try {
Systrace.beginSection(0, "splitRangesOnEmojis");
for (MarkdownRange range : oldRanges) {
if (range.getType().equals("emoji")) {
emojiRanges.add(range);
}
}

int i = 0;
int j = 0;
while (i < oldRanges.size()) {
MarkdownRange currentRange = oldRanges.get(i);
if (!currentRange.getType().equals(type)) {
newRanges.add(currentRange);
i += 1;
continue;
}

// Iterate through all emoji ranges before the end of the current range, splitting the current range at each intersection.
while (j < emojiRanges.size()) {
MarkdownRange emojiRange = emojiRanges.get(j);
if (emojiRange.getStart() > currentRange.getEnd()) {
break;
}

int currentStart = currentRange.getStart();
int currentEnd = currentRange.getEnd();
int emojiStart = emojiRange.getStart();
int emojiEnd = emojiRange.getEnd();
if (emojiStart >= currentStart && emojiEnd <= currentEnd) { // Intersection
MarkdownRange newRange = new MarkdownRange(currentRange.getType(), currentStart, emojiStart - currentStart, currentRange.getDepth());
currentRange = new MarkdownRange(currentRange.getType(), emojiEnd, currentEnd - emojiEnd, currentRange.getDepth());

if (newRange.getLength() > 0) {
newRanges.add(newRange);
}
}
j += 1;
}

if (currentRange.getLength() > 0) {
newRanges.add(currentRange);
}
i += 1;
}
} finally {
Systrace.endSection(0);
}

return newRanges;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.expensify.livemarkdown;

import static com.expensify.livemarkdown.RangeSplitter.splitRangesOnEmojis;

import static org.junit.Assert.assertEquals;

import org.junit.Test;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class RangeSplitterTest {

@Test
public void testNoOverlap() {
List<MarkdownRange> markdownRanges = new ArrayList<>();
markdownRanges.add(new MarkdownRange("strikethrough", 0, 10, 1));
markdownRanges.add(new MarkdownRange("emoji", 12, 2, 1));

markdownRanges = splitRangesOnEmojis(markdownRanges, "strikethrough");

assertEquals(2, markdownRanges.size());
assertEquals(new MarkdownRange("strikethrough", 0, 10, 1), markdownRanges.get(0));
assertEquals(new MarkdownRange("emoji", 12, 2, 1), markdownRanges.get(1));
}

@Test
public void testOverlapDifferentType() {
List<MarkdownRange> markdownRanges = new ArrayList<>();
markdownRanges.add(new MarkdownRange("strikethrough", 0, 10, 1));
markdownRanges.add(new MarkdownRange("emoji", 3, 4, 1));

markdownRanges = splitRangesOnEmojis(markdownRanges, "italic");

assertEquals(2, markdownRanges.size());
assertEquals(new MarkdownRange("strikethrough", 0, 10, 1), markdownRanges.get(0));
assertEquals(new MarkdownRange("emoji", 3, 4, 1), markdownRanges.get(1));
}

@Test
public void testSingleOverlap() {
List<MarkdownRange> markdownRanges = new ArrayList<>();
markdownRanges.add(new MarkdownRange("strikethrough", 0, 10, 1));
markdownRanges.add(new MarkdownRange("emoji", 3, 4, 1)); // This range should split the strikethrough range

markdownRanges = splitRangesOnEmojis(markdownRanges, "strikethrough");

// Sort is needed because ranges may get mixed while splitting
Collections.sort(markdownRanges, (r1, r2) -> Integer.compare(r1.getStart(), r2.getStart()));

assertEquals(3, markdownRanges.size());
assertEquals(new MarkdownRange("strikethrough", 0, 3, 1), markdownRanges.get(0));
assertEquals(new MarkdownRange("emoji", 3, 4, 1), markdownRanges.get(1));
assertEquals(new MarkdownRange("strikethrough", 7, 3, 1), markdownRanges.get(2));
}

@Test
public void testMultipleOverlapsMultipleTypes() {
List<MarkdownRange> markdownRanges = new ArrayList<>();
markdownRanges.add(new MarkdownRange("italic", 0, 20, 1));
markdownRanges.add(new MarkdownRange("strikethrough", 2, 12, 1));
markdownRanges.add(new MarkdownRange("emoji", 3, 1, 1));
markdownRanges.add(new MarkdownRange("emoji", 8, 2, 1));
markdownRanges.add(new MarkdownRange("strikethrough", 22, 5, 1));

markdownRanges = splitRangesOnEmojis(markdownRanges, "strikethrough");

// Sort is needed because ranges may get mixed while splitting
Collections.sort(markdownRanges, (r1, r2) -> Integer.compare(r1.getStart(), r2.getStart()));

assertEquals(7, markdownRanges.size());
assertEquals(new MarkdownRange("italic", 0, 20, 1), markdownRanges.get(0));
assertEquals(new MarkdownRange("strikethrough", 2, 1, 1), markdownRanges.get(1));
assertEquals(new MarkdownRange("emoji", 3, 1, 1), markdownRanges.get(2));
assertEquals(new MarkdownRange("strikethrough", 4, 4, 1), markdownRanges.get(3));
assertEquals(new MarkdownRange("emoji", 8, 2, 1), markdownRanges.get(4));
assertEquals(new MarkdownRange("strikethrough", 10, 4, 1), markdownRanges.get(5));
assertEquals(new MarkdownRange("strikethrough", 22, 5, 1), markdownRanges.get(6));
}
}
2 changes: 2 additions & 0 deletions src/parseExpensiMark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,8 @@ function getTagPriority(tag: string) {
return 2;
case 'h1':
return 1;
case 'emoji':
return -1;
default:
return 0;
}
Expand Down
9 changes: 7 additions & 2 deletions src/web/utils/blockUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ function addStyleToBlock(targetElement: HTMLElement, type: NodeType, markdownSty
node.style.textDecoration = 'line-through';
break;
case 'emoji':
Object.assign(node.style, {...markdownStyle.emoji, verticalAlign: 'middle'});
Object.assign(node.style, {
...markdownStyle.emoji,
verticalAlign: 'middle',
fontStyle: 'normal',
textDecoration: 'none',
display: 'inline-block',
});
break;
case 'mention-here':
Object.assign(node.style, markdownStyle.mentionHere);
Expand All @@ -49,7 +55,6 @@ function addStyleToBlock(targetElement: HTMLElement, type: NodeType, markdownSty
case 'pre':
Object.assign(node.style, markdownStyle.pre);
break;

case 'blockquote':
Object.assign(node.style, {
...markdownStyle.blockquote,
Expand Down
Loading