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

Centralize ExpensiMark usage with a dedicated Parser module #44732

Merged
10 changes: 7 additions & 3 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const restrictedImportPaths = [
'',
"For 'useWindowDimensions', please use '@src/hooks/useWindowDimensions' instead.",
"For 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable', please use 'PressableWithFeedback' and/or 'PressableWithoutFeedback' from '@components/Pressable' instead.",
"For 'StatusBar', please use '@src/libs/StatusBar' instead.",
"For 'StatusBar', please use '@libs/StatusBar' instead.",
"For 'Text', please use '@components/Text' instead.",
"For 'ScrollView', please use '@components/ScrollView' instead.",
].join('\n'),
Expand Down Expand Up @@ -59,8 +59,12 @@ const restrictedImportPaths = [
},
{
name: 'expensify-common',
importNames: ['Device'],
message: "Do not import Device directly, it's known to make VSCode's IntelliSense crash. Please import the desired module from `expensify-common/dist/Device` instead.",
importNames: ['Device', 'ExpensiMark'],
message: [
'',
"For 'Device', do not import it directly, it's known to make VSCode's IntelliSense crash. Please import the desired module from `expensify-common/dist/Device` instead.",
"For 'ExpensiMark', please use '@libs/Parser' instead.",
].join('\n'),
blazejkustra marked this conversation as resolved.
Show resolved Hide resolved
},
];

Expand Down
4 changes: 2 additions & 2 deletions src/components/LHNOptionsList/OptionRowLHN.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import DateUtils from '@libs/DateUtils';
import DomUtils from '@libs/DomUtils';
import {parseHtmlToText} from '@libs/OnyxAwareParser';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import Parser from '@libs/Parser';
import Performance from '@libs/Performance';
import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
import * as ReportUtils from '@libs/ReportUtils';
Expand Down Expand Up @@ -251,7 +251,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti
numberOfLines={1}
accessibilityLabel={translate('accessibilityHints.lastChatMessagePreview')}
>
{parseHtmlToText(optionItem.alternateText)}
{Parser.htmlToText(optionItem.alternateText)}
</Text>
) : null}
</View>
Expand Down
8 changes: 3 additions & 5 deletions src/components/MenuItem.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import {ExpensiMark} from 'expensify-common';
import type {ImageContentFit} from 'expo-image';
import type {ReactElement, ReactNode} from 'react';
import React, {forwardRef, useContext, useMemo} from 'react';
Expand All @@ -14,6 +13,7 @@ import ControlSelection from '@libs/ControlSelection';
import convertToLTR from '@libs/convertToLTR';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import getButtonState from '@libs/getButtonState';
import Parser from '@libs/Parser';
import type {AvatarSource} from '@libs/UserUtils';
import variables from '@styles/variables';
import * as Session from '@userActions/Session';
Expand Down Expand Up @@ -433,16 +433,14 @@ function MenuItem(
if (!title || !shouldParseTitle) {
return '';
}
const parser = new ExpensiMark();
return parser.replace(title, {shouldEscapeText});
return Parser.replace(title, {shouldEscapeText});
}, [title, shouldParseTitle, shouldEscapeText]);

const helperHtml = useMemo(() => {
if (!helperText || !shouldParseHelperText) {
return '';
}
const parser = new ExpensiMark();
return parser.replace(helperText, {shouldEscapeText});
return Parser.replace(helperText, {shouldEscapeText});
}, [helperText, shouldParseHelperText, shouldEscapeText]);

const processedTitle = useMemo(() => {
Expand Down
6 changes: 3 additions & 3 deletions src/hooks/useCopySelectionHelper.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {useEffect} from 'react';
import Clipboard from '@libs/Clipboard';
import KeyboardShortcut from '@libs/KeyboardShortcut';
import {parseHtmlToMarkdown, parseHtmlToText} from '@libs/OnyxAwareParser';
import Parser from '@libs/Parser';
import SelectionScraper from '@libs/SelectionScraper';
import CONST from '@src/CONST';

Expand All @@ -11,10 +11,10 @@ function copySelectionToClipboard() {
return;
}
if (!Clipboard.canSetHtml()) {
Clipboard.setString(parseHtmlToMarkdown(selection));
Clipboard.setString(Parser.htmlToMarkdown(selection));
return;
}
Clipboard.setHtml(selection, parseHtmlToText(selection));
Clipboard.setHtml(selection, Parser.htmlToText(selection));
}

export default function useCopySelectionHelper() {
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useHtmlPaste/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {useNavigation} from '@react-navigation/native';
import {useCallback, useEffect} from 'react';
import {parseHtmlToMarkdown} from '@libs/OnyxAwareParser';
import Parser from '@libs/Parser';
import type UseHtmlPaste from './types';

const insertByCommand = (text: string) => {
Expand Down Expand Up @@ -71,7 +71,7 @@ const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, removeLi
*/
const handlePastedHTML = useCallback(
(html: string) => {
paste(parseHtmlToMarkdown(html));
paste(Parser.htmlToMarkdown(html));
},
[paste],
);
Expand Down
3 changes: 1 addition & 2 deletions src/libs/Log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// action would likely cause confusion about which one to use. But most other API methods should happen inside an action file.

/* eslint-disable rulesdir/no-api-in-views */
import {ExpensiMark, Logger} from 'expensify-common';
import {Logger} from 'expensify-common';
import Onyx from 'react-native-onyx';
import type {Merge} from 'type-fest';
import CONST from '@src/CONST';
Expand Down Expand Up @@ -80,6 +80,5 @@ const Log = new Logger({
isDebug: true,
});
timeout = setTimeout(() => Log.info('Flushing logs older than 10 minutes', true, {}, true), 10 * 60 * 1000);
ExpensiMark.setLogger(Log);

export default Log;
45 changes: 0 additions & 45 deletions src/libs/OnyxAwareParser.ts

This file was deleted.

51 changes: 51 additions & 0 deletions src/libs/Parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// eslint-disable-next-line no-restricted-imports
import {ExpensiMark} from 'expensify-common';
import Onyx from 'react-native-onyx';
import ONYXKEYS from '@src/ONYXKEYS';
import Log from './Log';
import * as ReportConnection from './ReportConnection';

const accountIDToNameMap: Record<string, string> = {};

Onyx.connect({
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (personalDetailsList) => {
Object.values(personalDetailsList ?? {}).forEach((personalDetails) => {
if (!personalDetails) {
return;
}

accountIDToNameMap[personalDetails.accountID] = personalDetails.login ?? String(personalDetails.accountID);
});
},
});

type Extras = {
reportIDToName?: Record<string, string>;
accountIDToName?: Record<string, string>;
cacheVideoAttributes?: (vidSource: string, attrs: string) => void;
videoAttributeCache?: Record<string, string>;
};

class ExpensiMarkWithContext extends ExpensiMark {
Copy link
Contributor

Choose a reason for hiding this comment

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

NAB: Can we please have a comment at the top of the file, or above this class (your choice), which explains the purpose of this briefly? I think it will be helpful for future readers.

htmlToMarkdown(htmlString: string, extras?: Extras): string {
return super.htmlToMarkdown(htmlString, {
reportIDToName: extras?.reportIDToName ?? ReportConnection.getAllReportsNameMap(),
accountIDToName: extras?.accountIDToName ?? accountIDToNameMap,
cacheVideoAttributes: extras?.cacheVideoAttributes,
});
}

htmlToText(htmlString: string, extras?: Extras): string {
return super.htmlToText(htmlString, {
reportIDToName: extras?.reportIDToName ?? ReportConnection.getAllReportsNameMap(),
accountIDToName: extras?.accountIDToName ?? accountIDToNameMap,
cacheVideoAttributes: extras?.cacheVideoAttributes,
});
}
}

ExpensiMarkWithContext.setLogger(Log);
const Parser = new ExpensiMarkWithContext();

export default Parser;
6 changes: 3 additions & 3 deletions src/libs/ReportActionsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import isReportMessageAttachment from './isReportMessageAttachment';
import * as Localize from './Localize';
import Log from './Log';
import type {MessageElementBase, MessageTextElement} from './MessageElement';
import {parseHtmlToText} from './OnyxAwareParser';
import Parser from './Parser';
import * as PersonalDetailsUtils from './PersonalDetailsUtils';
import * as ReportConnection from './ReportConnection';
import type {OptimisticIOUReportAction, PartialReportAction} from './ReportUtils';
Expand Down Expand Up @@ -1080,11 +1080,11 @@ function getReportActionText(reportAction: PartialReportAction): string {
// Sometime html can be an empty string
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const text = (message?.html || message?.text) ?? '';
return text ? parseHtmlToText(text) : '';
return text ? Parser.htmlToText(text) : '';
}

function getTextFromHtml(html?: string): string {
return html ? parseHtmlToText(html) : '';
return html ? Parser.htmlToText(html) : '';
}

function getMemberChangeMessageFragment(reportAction: OnyxEntry<ReportAction>): Message {
Expand Down
17 changes: 8 additions & 9 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {format} from 'date-fns';
import {ExpensiMark, Str} from 'expensify-common';
import {Str} from 'expensify-common';
import {isEmpty} from 'lodash';
import lodashEscape from 'lodash/escape';
import lodashFindLastIndex from 'lodash/findLastIndex';
Expand Down Expand Up @@ -63,7 +63,7 @@ import ModifiedExpenseMessage from './ModifiedExpenseMessage';
import linkingConfig from './Navigation/linkingConfig';
import Navigation from './Navigation/Navigation';
import * as NumberUtils from './NumberUtils';
import {parseHtmlToText} from './OnyxAwareParser';
import Parser from './Parser';
import Permissions from './Permissions';
import * as PersonalDetailsUtils from './PersonalDetailsUtils';
import * as PhoneNumber from './PhoneNumber';
Expand Down Expand Up @@ -3302,7 +3302,7 @@ function parseReportActionHtmlToText(reportAction: OnyxEntry<ReportAction>, repo
const logins = PersonalDetailsUtils.getLoginsByAccountIDs(accountIDs);
accountIDs.forEach((id, index) => (accountIDToName[id] = logins[index]));

const textMessage = Str.removeSMSDomain(parseHtmlToText(html, reportIDToName, accountIDToName));
const textMessage = Str.removeSMSDomain(Parser.htmlToText(html, {reportIDToName, accountIDToName}));
parsedReportActionMessageCache[key] = textMessage;

return textMessage;
Expand Down Expand Up @@ -3657,7 +3657,6 @@ function getParsedComment(text: string, parsingDetails?: ParsingDetails): string
isGroupPolicyReport = isReportInGroupPolicy(currentReport);
}

const parser = new ExpensiMark();
const textWithMention = text.replace(CONST.REGEX.SHORT_MENTION, (match) => {
if (!Str.isValidMention(match)) {
return match;
Expand All @@ -3668,7 +3667,7 @@ function getParsedComment(text: string, parsingDetails?: ParsingDetails): string
});

return text.length <= CONST.MAX_MARKUP_LENGTH
? parser.replace(textWithMention, {shouldEscapeText: parsingDetails?.shouldEscapeText, disabledRules: isGroupPolicyReport ? [] : ['reportMentions']})
? Parser.replace(textWithMention, {shouldEscapeText: parsingDetails?.shouldEscapeText, disabledRules: isGroupPolicyReport ? [] : ['reportMentions']})
: lodashEscape(text);
}

Expand All @@ -3677,15 +3676,15 @@ function getReportDescriptionText(report: Report): string {
return '';
}

return parseHtmlToText(report.description);
return Parser.htmlToText(report.description);
}

function getPolicyDescriptionText(policy: OnyxEntry<Policy>): string {
if (!policy?.description) {
return '';
}

return parseHtmlToText(policy.description);
return Parser.htmlToText(policy.description);
}

function buildOptimisticAddCommentReportAction(
Expand All @@ -3707,10 +3706,10 @@ function buildOptimisticAddCommentReportAction(
textForNewComment = CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML;
} else if (isTextOnly) {
htmlForNewComment = commentText;
textForNewComment = parseHtmlToText(htmlForNewComment);
textForNewComment = Parser.htmlToText(htmlForNewComment);
} else {
htmlForNewComment = `${commentText}<uploading-attachment>${CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML}</uploading-attachment>`;
textForNewComment = `${parseHtmlToText(commentText)}\n${CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML}`;
textForNewComment = `${Parser.htmlToText(commentText)}\n${CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML}`;
}

const isAttachment = !text && file !== undefined;
Expand Down
4 changes: 2 additions & 2 deletions src/libs/actions/Policy/Member.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import {ExpensiMark} from 'expensify-common';
import type {NullishDeep, OnyxCollection, OnyxCollectionInputValue, OnyxEntry, OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import * as API from '@libs/API';
Expand All @@ -12,6 +11,7 @@ import type {
import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import * as ErrorUtils from '@libs/ErrorUtils';
import Log from '@libs/Log';
import Parser from '@libs/Parser';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as PhoneNumber from '@libs/PhoneNumber';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
Expand Down Expand Up @@ -627,7 +627,7 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount

const params: AddMembersToWorkspaceParams = {
employees: JSON.stringify(logins.map((login) => ({email: login}))),
welcomeNote: new ExpensiMark().replace(welcomeNote),
welcomeNote: Parser.replace(welcomeNote),
policyID,
};
if (!isEmptyObject(membersChats.reportCreationData)) {
Expand Down
Loading
Loading