Skip to content
Merged
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
1 change: 0 additions & 1 deletion src/__tests__/parseExpensiMark.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ test('emoji', () => {
test('emoji and italic', () => {
expect('_😎_').toBeParsedAs([
{type: 'syntax', start: 0, length: 1},
{type: 'italic', start: 1, length: 2},
{type: 'emoji', start: 1, length: 2},
{type: 'syntax', start: 3, length: 1},
]);
Expand Down
22 changes: 11 additions & 11 deletions src/__tests__/splitRangesOnEmojis.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {MarkdownRange} from '../commonTypes';
import {splitRangesOnEmojis} from '../rangeUtils';
import {excludeRangeTypesFromFormatting, getRangesToExcludeFormatting} from '../rangeUtils';

const sortRanges = (ranges: MarkdownRange[]) => {
return ranges.sort((a, b) => a.start - b.start);
Expand All @@ -11,7 +11,7 @@ test('no overlap', () => {
{type: 'emoji', start: 12, length: 2},
];

const splittedRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough');
const splittedRanges = excludeRangeTypesFromFormatting(markdownRanges, 'strikethrough', getRangesToExcludeFormatting(markdownRanges));
expect(splittedRanges).toEqual([
{type: 'strikethrough', start: 0, length: 10},
{type: 'emoji', start: 12, length: 2},
Expand All @@ -24,7 +24,7 @@ test('overlap different type', () => {
{type: 'emoji', start: 3, length: 4},
];

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

Expand All @@ -35,7 +35,7 @@ describe('single overlap', () => {
{type: 'emoji', start: 0, length: 2},
];

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

expect(markdownRanges).toEqual([
Expand All @@ -50,7 +50,7 @@ describe('single overlap', () => {
{type: 'emoji', start: 3, length: 4},
];

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

expect(markdownRanges).toEqual([
Expand All @@ -66,7 +66,7 @@ describe('single overlap', () => {
{type: 'emoji', start: 8, length: 2},
];

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

expect(markdownRanges).toEqual([
Expand All @@ -82,7 +82,7 @@ describe('single overlap', () => {
{type: 'emoji', start: 5, length: 2},
];

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

expect(markdownRanges).toEqual([
Expand All @@ -101,7 +101,7 @@ describe('single overlap', () => {
{type: 'emoji', start: 4, length: 2},
];

markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough');
markdownRanges = excludeRangeTypesFromFormatting(markdownRanges, 'strikethrough', getRangesToExcludeFormatting(markdownRanges));

expect(markdownRanges).toEqual([
{type: 'emoji', start: 0, length: 2},
Expand All @@ -121,7 +121,7 @@ describe('multiple overlaps', () => {
{type: 'strikethrough', start: 22, length: 5},
];

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

expect(markdownRanges).toEqual([
Expand All @@ -144,8 +144,8 @@ describe('multiple overlaps', () => {
{type: 'strikethrough', start: 22, length: 5},
];

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

expect(markdownRanges).toEqual([
Expand Down
15 changes: 6 additions & 9 deletions src/parseExpensiMark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +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 {groupRanges, sortRanges, splitRangesOnEmojis} from './rangeUtils';
import {groupRanges, sortRanges, excludeRangeTypesFromFormatting, getRangesToExcludeFormatting} from './rangeUtils';

function isWeb() {
return Platform.OS === 'web';
Expand Down Expand Up @@ -238,8 +238,6 @@ function parseTreeToTextAndRanges(tree: StackItem): [string, MarkdownRange[]] {
return [text, ranges];
}

const isNative = Platform.OS === 'android' || Platform.OS === 'ios';

function parseExpensiMark(markdown: string): MarkdownRange[] {
if (markdown.length > MAX_PARSABLE_LENGTH) {
return [];
Expand All @@ -257,12 +255,11 @@ function parseExpensiMark(markdown: string): MarkdownRange[] {
return [];
}
let markdownRanges = sortRanges(ranges);
if (isNative) {
// Blocks applying italic and strikethrough styles to emojis on Android and iOS
// TODO: Remove this condition when splitting emojis inside the inline code block will be fixed on the web
markdownRanges = splitRangesOnEmojis(markdownRanges, 'italic');
markdownRanges = splitRangesOnEmojis(markdownRanges, 'strikethrough');
}

// Prevent italic and strikethrough formatting inside emojis and inline code blocks
const rangesToExclude = getRangesToExcludeFormatting(markdownRanges);
markdownRanges = excludeRangeTypesFromFormatting(markdownRanges, 'italic', rangesToExclude);
markdownRanges = excludeRangeTypesFromFormatting(markdownRanges, 'strikethrough', rangesToExclude);

const groupedRanges = groupRanges(markdownRanges);
return groupedRanges;
Expand Down
52 changes: 38 additions & 14 deletions src/rangeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,33 @@ function ungroupRanges(ranges: MarkdownRange[]): MarkdownRange[] {
return ungroupedRanges;
}

function splitRangesOnEmojis(ranges: MarkdownRange[], type: MarkdownType): MarkdownRange[] {
const emojiRanges: MarkdownRange[] = ranges.filter((range) => range.type === 'emoji');
/**
* Creates a list of ranges that should not be formatted by certain markdown types (italic, strikethrough).
* This includes emojis and syntaxes of inline code blocks.
*/
function getRangesToExcludeFormatting(ranges: MarkdownRange[]) {
let closingSyntaxPosition: number | null = null;
return ranges.filter((range, index) => {
const nextRange = ranges[index + 1];
if (nextRange && nextRange.type === 'code' && range.type === 'syntax') {
closingSyntaxPosition = nextRange.start + nextRange.length;
return true;
}
if (closingSyntaxPosition !== null && range.type === 'syntax' && range.start <= closingSyntaxPosition) {
closingSyntaxPosition = null;
return true;
}
return range.type === 'emoji';
});
}

/**
* Splits ranges of a specific type from being formatted by specified markdown types (e.g., 'emoji', 'syntax').
* @param ranges - The array of MarkdownRange objects to process.
* @param baseMarkdownType - The base markdown type to exclude formatting from (e.g., 'italic').
* @param rangesToExclude - The array of MarkdownRange objects representing the ranges to exclude from formatting.
*/
function excludeRangeTypesFromFormatting(ranges: MarkdownRange[], baseMarkdownType: MarkdownType, rangesToExclude: MarkdownRange[]): MarkdownRange[] {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NAB but baseMarkdownType could be an array of types maybe ? would save one loop over rangesToExclude and one line in parseExpensiMark :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would require more changes in the current algorithm. Right now, we need to execute excludeRangeTypesFromFormatting one after another, because otherwise, split ranges start to overlap each other, and it breaks the markdown rendering then

const newRanges: MarkdownRange[] = [];

let i = 0;
Expand All @@ -69,33 +94,32 @@ function splitRangesOnEmojis(ranges: MarkdownRange[], type: MarkdownType): Markd
break;
}

if (currentRange.type !== type) {
if (currentRange.type !== baseMarkdownType) {
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) {
while (j < rangesToExclude.length) {
const excludeRange = rangesToExclude[j];
if (!excludeRange || excludeRange.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;
const excludeRangeStart: number = excludeRange.start;
const excludeRangeEnd: number = excludeRange.start + excludeRange.length;

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

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

if (newRange.length > 0) {
newRanges.push(newRange);
Expand All @@ -113,4 +137,4 @@ function splitRangesOnEmojis(ranges: MarkdownRange[], type: MarkdownType): Markd
return newRanges;
}

export {sortRanges, groupRanges, ungroupRanges, splitRangesOnEmojis};
export {sortRanges, groupRanges, ungroupRanges, excludeRangeTypesFromFormatting, getRangesToExcludeFormatting};
3 changes: 0 additions & 3 deletions src/web/utils/blockUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ function addStyleToBlock(targetElement: HTMLElement, type: NodeType, markdownSty
Object.assign(node.style, {
...markdownStyle.emoji,
verticalAlign: 'middle',
fontStyle: 'normal', // remove italic
textDecoration: 'none', // remove strikethrough
display: 'inline-block',
});
break;
case 'mention-here':
Expand Down