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 #591

Merged
merged 9 commits into from
Jan 7, 2025
163 changes: 163 additions & 0 deletions src/__tests__/splitRangesOnEmojis.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import type {MarkdownRange} from '../commonTypes';
import {splitRangesOnEmojis} from '../rangeUtils';

const sortRanges = (ranges: MarkdownRange[]) => {
return ranges.sort((a, b) => a.start - b.start);
};

test('no overlap', () => {
const markdownRanges: MarkdownRange[] = [
{type: 'strikethrough', start: 0, length: 10},
{type: 'emoji', start: 12, length: 2},
];

const splittedRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough');
expect(splittedRanges).toEqual([
{type: 'strikethrough', start: 0, length: 10},
{type: 'emoji', start: 12, length: 2},
]);
});

test('overlap different type', () => {
const markdownRanges: MarkdownRange[] = [
{type: 'strikethrough', start: 0, length: 10},
{type: 'emoji', start: 3, length: 4},
];

const splittedRanges = splitRangesOnEmojis(markdownRanges, 'italic');
expect(splittedRanges).toEqual(markdownRanges);
});

describe('single overlap', () => {
test('emoji at the beginning', () => {
let markdownRanges: MarkdownRange[] = [
{type: 'strikethrough', start: 0, length: 10},
{type: 'emoji', start: 0, length: 2},
];

markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough');
sortRanges(markdownRanges);

expect(markdownRanges).toEqual([
{type: 'emoji', start: 0, length: 2},
{type: 'strikethrough', start: 2, length: 8},
]);
});

test('emoji in the middle', () => {
let markdownRanges: MarkdownRange[] = [
{type: 'strikethrough', start: 0, length: 10},
{type: 'emoji', start: 3, length: 4},
];

markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough');
sortRanges(markdownRanges);

expect(markdownRanges).toEqual([
{type: 'strikethrough', start: 0, length: 3},
{type: 'emoji', start: 3, length: 4},
{type: 'strikethrough', start: 7, length: 3},
]);
});

test('emoji at the end', () => {
let markdownRanges: MarkdownRange[] = [
{type: 'strikethrough', start: 0, length: 10},
{type: 'emoji', start: 8, length: 2},
];

markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough');
sortRanges(markdownRanges);

expect(markdownRanges).toEqual([
{type: 'strikethrough', start: 0, length: 8},
{type: 'emoji', start: 8, length: 2},
]);
});

test('multiple emojis in the middle', () => {
let markdownRanges: MarkdownRange[] = [
{type: 'strikethrough', start: 0, length: 10},
{type: 'emoji', start: 3, length: 2},
{type: 'emoji', start: 5, length: 2},
];

markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough');
sortRanges(markdownRanges);

expect(markdownRanges).toEqual([
{type: 'strikethrough', start: 0, length: 3},
{type: 'emoji', start: 3, length: 2},
{type: 'emoji', start: 5, length: 2},
{type: 'strikethrough', start: 7, length: 3},
]);
});

test('just emojis', () => {
let markdownRanges: MarkdownRange[] = [
{type: 'strikethrough', start: 0, length: 6},
{type: 'emoji', start: 0, length: 2},
{type: 'emoji', start: 2, length: 2},
{type: 'emoji', start: 4, length: 2},
];

markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough');

expect(markdownRanges).toEqual([
{type: 'emoji', start: 0, length: 2},
{type: 'emoji', start: 2, length: 2},
{type: 'emoji', start: 4, length: 2},
]);
});
});

describe('multiple overlaps', () => {
test('splitting on one type', () => {
let markdownRanges: MarkdownRange[] = [
{type: 'italic', start: 0, length: 20},
{type: 'strikethrough', start: 2, length: 12},
{type: 'emoji', start: 3, length: 1},
{type: 'emoji', start: 8, length: 2},
{type: 'strikethrough', start: 22, length: 5},
];

markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough');
sortRanges(markdownRanges);

expect(markdownRanges).toEqual([
{type: 'italic', start: 0, length: 20},
{type: 'strikethrough', start: 2, length: 1},
{type: 'emoji', start: 3, length: 1},
{type: 'strikethrough', start: 4, length: 4},
{type: 'emoji', start: 8, length: 2},
{type: 'strikethrough', start: 10, length: 4},
{type: 'strikethrough', start: 22, length: 5},
]);
});

test('splitting on two types', () => {
let markdownRanges: MarkdownRange[] = [
{type: 'italic', start: 0, length: 20},
{type: 'strikethrough', start: 2, length: 12},
{type: 'emoji', start: 3, length: 1},
{type: 'emoji', start: 8, length: 2},
{type: 'strikethrough', start: 22, length: 5},
];

markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough');
markdownRanges = splitRangesOnEmojis(markdownRanges, 'italic');
sortRanges(markdownRanges);

expect(markdownRanges).toEqual([
{type: 'italic', start: 0, length: 3},
{type: 'strikethrough', start: 2, length: 1},
{type: 'emoji', start: 3, length: 1},
{type: 'italic', start: 4, length: 4},
{type: 'strikethrough', start: 4, length: 4},
{type: 'emoji', start: 8, length: 2},
{type: 'italic', start: 10, length: 10},
{type: 'strikethrough', start: 10, length: 4},
{type: 'strikethrough', start: 22, length: 5},
]);
});
});
10 changes: 9 additions & 1 deletion src/parseExpensiMark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {unescapeText} from 'expensify-common/dist/utils';
import {decode} from 'html-entities';
import type {WorkletFunction} from 'react-native-reanimated/lib/typescript/commonTypes';
import type {MarkdownType, MarkdownRange} from './commonTypes';
import {splitRangesOnEmojis} from './rangeUtils';

function isWeb() {
return Platform.OS === 'web';
Expand Down Expand Up @@ -236,6 +237,8 @@ function parseTreeToTextAndRanges(tree: StackItem): [string, MarkdownRange[]] {
// getTagPriority returns a priority for a tag, higher priority means the tag should be processed first
function getTagPriority(tag: string) {
switch (tag) {
case 'syntax': // syntax has the lowest priority so other styles can be applied to it
return -1;
case 'blockquote':
return 2;
case 'h1':
Expand Down Expand Up @@ -287,8 +290,13 @@ function parseExpensiMark(markdown: string): MarkdownRange[] {
)}'\nOriginal input: '${JSON.stringify(markdown)}'`,
);
}
const sortedRanges = sortRanges(ranges);

let splittedRanges = splitRangesOnEmojis(ranges, 'italic');
splittedRanges = splitRangesOnEmojis(splittedRanges, 'strikethrough');

const sortedRanges = sortRanges(splittedRanges);
const groupedRanges = groupRanges(sortedRanges);

return groupedRanges;
}

Expand Down
62 changes: 62 additions & 0 deletions src/rangeUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
'worklet';

import type {MarkdownRange, MarkdownType} from './commonTypes';

function splitRangesOnEmojis(ranges: MarkdownRange[], type: MarkdownType): MarkdownRange[] {
const emojiRanges: MarkdownRange[] = ranges.filter((range) => range.type === 'emoji');
const newRanges: MarkdownRange[] = [];

let i = 0;
let j = 0;
while (i < ranges.length) {
const currentRange = ranges[i];
if (!currentRange) {
break;
}

if (currentRange.type !== type) {
newRanges.push(currentRange);
i++;
} else {
// Iterate through all emoji ranges before the end of the current range, splitting the current range at each intersection.
while (j < emojiRanges.length) {
const emojiRange = emojiRanges[j];
if (!emojiRange || emojiRange.start > currentRange.start + currentRange.length) {
break;
}

const currentStart: number = currentRange.start;
const currentEnd: number = currentRange.start + currentRange.length;
const emojiStart: number = emojiRange.start;
const emojiEnd: number = emojiRange.start + emojiRange.length;

if (emojiStart >= currentStart && emojiEnd <= currentEnd) {
// Intersection
const newRange: MarkdownRange = {
type: currentRange.type,
start: currentStart,
length: emojiStart - currentStart,
...(currentRange?.depth && {depth: currentRange?.depth}),
};

currentRange.start = emojiEnd;
currentRange.length = currentEnd - emojiEnd;

if (newRange.length > 0) {
newRanges.push(newRange);
}
}
j++;
}

if (currentRange.length > 0) {
newRanges.push(currentRange);
}
i++;
}
}
return newRanges;
}

// eslint-disable-next-line import/prefer-default-export
export {splitRangesOnEmojis};
Loading