Skip to content
Draft
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
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
"@expensify/nitro-utils": "file:./modules/ExpensifyNitroUtils",
"@expensify/react-native-background-task": "file:./modules/background-task",
"@expensify/react-native-hybrid-app": "file:./modules/hybrid-app",
"@expensify/react-native-live-markdown": "0.1.336",
"@expensify/react-native-live-markdown": "0.1.337",
"@expensify/react-native-wallet": "0.1.22",
"@expo/metro-config": "57.0.7",
"@expo/metro-runtime": "57.0.7",
Expand Down
6 changes: 3 additions & 3 deletions src/libs/EmojiUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ const findEmojiByCode = (code: string): Emoji => Emojis.emojiCodeTableWithSkinTo
const CODE_RANGE_TYPES = new Set(['code', 'pre']);

function getCodeRanges(text: string): MarkdownRange[] {
return parseExpensiMark(text).filter((range) => CODE_RANGE_TYPES.has(range.type));
return parseExpensiMark(text, CONST.MAX_MARKUP_LENGTH).filter((range) => CODE_RANGE_TYPES.has(range.type));
}

function isPositionInsideCodeRanges(ranges: MarkdownRange[], position: number): boolean {
return ranges.some((range) => CODE_RANGE_TYPES.has(range.type) && position >= range.start && position < range.start + range.length);
}

function isPositionInsideCodeBlock(text: string, position: number): boolean {
return isPositionInsideCodeRanges(parseExpensiMark(text), position);
return isPositionInsideCodeRanges(parseExpensiMark(text, CONST.MAX_MARKUP_LENGTH), position);
}

/**
Expand Down Expand Up @@ -462,7 +462,7 @@ function replaceEmojis(text: string, preferredSkinTone: OnyxEntry<number | strin
return {text: revertEmojisInCodeBlocks(newText).text, emojis};
}

const codeBlockRanges = parseExpensiMark(text);
const codeBlockRanges = parseExpensiMark(text, CONST.MAX_MARKUP_LENGTH);
const replacements: Array<{position: number; shortcode: string; replacement: string; name: string}> = [];
const shortcodeSearchPositions: Record<string, number> = {};
const englishTrie = normalizedLocale !== CONST.LOCALES.DEFAULT ? getEmojiTrie(CONST.LOCALES.DEFAULT) : null;
Expand Down
4 changes: 3 additions & 1 deletion src/libs/FormatSelectionUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import CONST from '@src/CONST';

import type {MarkdownType} from '@expensify/react-native-live-markdown';

import {parseExpensiMark} from '@expensify/react-native-live-markdown';
Expand All @@ -23,7 +25,7 @@ function applyFormatting(text: string, selectionStart: number, selectionEnd: num
}

function findMatchingFormat(text: string, selectionStart: number, selectionEnd: number, formatRule: FormatRule): Match | null {
const markdownRanges = parseExpensiMark(text);
const markdownRanges = parseExpensiMark(text, CONST.MAX_MARKUP_LENGTH);
for (const range of markdownRanges) {
if (range?.type === formatRule.markdownType && range.start != null && range.length != null) {
const rangeEnd = range.start + range.length;
Expand Down
2 changes: 1 addition & 1 deletion src/libs/ParsingUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ function decorateRangesWithShortMentions(ranges: MarkdownRange[], text: string,
function parseExpensiMarkWithShortMentions(text: string, availableMentions: string[], currentUserMentions?: string[]) {
'worklet';

const parsedRanges = parseExpensiMark(text);
const parsedRanges = parseExpensiMark(text, CONST.MAX_MARKUP_LENGTH);
return decorateRangesWithShortMentions(parsedRanges, text, availableMentions, currentUserMentions);
}

Expand Down
20 changes: 20 additions & 0 deletions tests/unit/EmojiTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as Browser from '@libs/Browser';
import emojiTrieForLocale, {buildEmojisTrie} from '@libs/EmojiTrie';
import * as EmojiUtils from '@libs/EmojiUtils';

import CONST from '@src/CONST';
import type FrequentlyUsedEmoji from '@src/types/onyx/FrequentlyUsedEmoji';
import type {ReportActionReaction} from '@src/types/onyx/ReportActionReactions';

Expand Down Expand Up @@ -176,6 +177,12 @@ describe('EmojiTest', () => {
expect(EmojiUtils.replaceEmojis(text).text).toBe('`:smile:`');
});

it('should revert emoji unicode inside a code block at the maximum markup length', () => {
const code = '`😄`';
const prefix = 'a'.repeat(CONST.MAX_MARKUP_LENGTH - code.length);
expect(EmojiUtils.replaceEmojis(`${prefix}${code}`).text).toBe(`${prefix}\`:smile:\``);
});

it('should revert multiple emojis inside code block', () => {
const text = '`😄👋`';
expect(EmojiUtils.replaceEmojis(text).text).toBe('`:smile::wave:`');
Expand All @@ -191,6 +198,12 @@ describe('EmojiTest', () => {
expect(EmojiUtils.replaceEmojis(text).text).toBe('👋 hello `:smile:` world');
});

it('should replace a shortcode outside code but preserve one inside code at the maximum markup length', () => {
const markdown = '\n:smile: and `:wave:`';
const prefix = 'a'.repeat(CONST.MAX_MARKUP_LENGTH - markdown.length);
expect(EmojiUtils.replaceEmojis(`${prefix}${markdown}`).text).toBe(`${prefix}\n😄 and \`:wave:\``);
});

it('should handle same shortcode both inside and outside code block', () => {
// Regression test: indexOf was returning the first occurrence for both,
// causing the shortcode outside to not be converted
Expand Down Expand Up @@ -284,6 +297,13 @@ describe('EmojiTest', () => {
expect(EmojiUtils.isPositionInsideCodeBlock(text, 1)).toBe(true);
});

it('should return true for a position inside inline code at the maximum markup length', () => {
const code = '`:smi`';
const prefix = 'a'.repeat(CONST.MAX_MARKUP_LENGTH - code.length);
const text = `${prefix}${code}`;
expect(EmojiUtils.isPositionInsideCodeBlock(text, prefix.length + 1)).toBe(true);
});

it('should return false for position outside code block', () => {
const text = 'hello `:joy:`';
// Position 0 is 'h' which is outside
Expand Down
8 changes: 8 additions & 0 deletions tests/unit/FormatSelectionUtilsTest.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import toggleSelectionFormat from '@libs/FormatSelectionUtils';

import CONST from '@src/CONST';

jest.unmock('@expensify/react-native-live-markdown');

describe('FormatSelectionUtils', () => {
Expand Down Expand Up @@ -79,6 +81,12 @@ describe('FormatSelectionUtils', () => {
expect(toggleSelectionFormat('_aaa_ _*bbb*_ _ccc_', 7, 12, 'formatItalic')).toEqual({updatedText: '_aaa_ *bbb* _ccc_', cursorOffset: -1});
});

it('remove formatting from a long Markdown range', () => {
const text = `*${'a'.repeat(CONST.MAX_MARKUP_LENGTH - 2)}*`;

expect(toggleSelectionFormat(text, 1, text.length - 1, 'formatBold')).toEqual({updatedText: 'a'.repeat(CONST.MAX_MARKUP_LENGTH - 2), cursorOffset: -1});
});

it('do nothing for unsupported command', () => {
expect(toggleSelectionFormat('aaa', 0, 3, 'formatUnderline')).toEqual({updatedText: 'aaa', cursorOffset: 0});
expect(toggleSelectionFormat('_aaa_', 1, 4, 'formatUnderline')).toEqual({updatedText: '_aaa_', cursorOffset: 0});
Expand Down
16 changes: 15 additions & 1 deletion tests/unit/libs/ParsingUtilsTest.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import {decorateRangesWithShortMentions, getParsedMessageWithShortMentions} from '@libs/ParsingUtils';
import {decorateRangesWithShortMentions, getParsedMessageWithShortMentions, parseExpensiMarkWithShortMentions} from '@libs/ParsingUtils';

import CONST from '@src/CONST';

import type {MarkdownRange} from '@expensify/react-native-live-markdown';

const TEST_COMPANY_DOMAIN = 'myCompany.com';

jest.unmock('@expensify/react-native-live-markdown');

describe('decorateRangesWithShortMentions', () => {
test('returns empty list for empty text', () => {
const result = decorateRangesWithShortMentions([], '', [], []);
Expand Down Expand Up @@ -223,3 +227,13 @@ describe('getParsedMessageWithShortMentions', () => {
expect(result).toEqual(`this is <mention-user>@john.doe@myCompany.com</mention-user>&#x27;s mention`);
});
});

describe('parseExpensiMarkWithShortMentions', () => {
test('parses Markdown up to the App markup limit', () => {
const text = `*${'a'.repeat(CONST.MAX_MARKUP_LENGTH - 2)}*`;

const result = parseExpensiMarkWithShortMentions(text, [], []);

expect(result).toContainEqual({type: 'bold', start: 1, length: CONST.MAX_MARKUP_LENGTH - 2});
});
});
Loading