Skip to content

Commit

Permalink
Merge branch 'main' into chirag-deprecating-sequenceNumber
Browse files Browse the repository at this point in the history
  • Loading branch information
chiragsalian committed Oct 20, 2023
2 parents 32ac959 + 6517be4 commit 29f3255
Show file tree
Hide file tree
Showing 8 changed files with 329 additions and 45 deletions.
229 changes: 223 additions & 6 deletions __tests__/ExpensiMark-HTML-test.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions __tests__/ExpensiMark-Markdown-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -554,15 +554,15 @@ test('Test html to heading1 markdown when there are other tags inside h1 tag', (
test('Test html to heading1 markdown when h1 tag is in the beginning of the line', () => {
const testString = '<h1>heading1</h1> in the beginning of the line';
const resultString = '# heading1\n'
+ 'in the beginning of the line';
+ ' in the beginning of the line';
expect(parser.htmlToMarkdown(testString)).toBe(resultString);
});

test('Test html to heading1 markdown when h1 tags are in the middle of the line', () => {
const testString = 'this line has a <h1>heading1</h1> in the middle of the line';
const resultString = 'this line has a\n'
+ '# heading1\n'
+ 'in the middle of the line';
+ ' in the middle of the line';
expect(parser.htmlToMarkdown(testString)).toBe(resultString);
});

Expand Down
12 changes: 12 additions & 0 deletions __tests__/ExpensiMark-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,15 @@ test('Test with regex Maximum regex stack depth reached error', () => {
expect(parser.getRemovedMarkdownLinks(testString, 'google.com')).toStrictEqual([]);
expect(extractLinksInMarkdownCommentMock).toHaveBeenCalledTimes(3);
});

test('Test extract link with ending parentheses', () => {
const comment = '[Staging Detail](https://staging.new.expensify.com/details) [Staging Detail](https://staging.new.expensify.com/details)) [Staging Detail](https://staging.new.expensify.com/details)))';
const links = ['https://staging.new.expensify.com/details', 'https://staging.new.expensify.com/details', 'https://staging.new.expensify.com/details'];
expect(parser.extractLinksInMarkdownComment(comment)).toStrictEqual(links);
});

test('Test extract link from Markdown link syntax', () => {
const comment = 'www.google.com https://www.google.com [Expensify](https://new.expensify.com/)';
const links = ['https://new.expensify.com/'];
expect(parser.extractLinksInMarkdownComment(comment)).toStrictEqual(links);
});
4 changes: 2 additions & 2 deletions lib/CONST.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ export declare const CONST: {
readonly CLOUDFRONT: "https://d2k5nsl2zxldvw.cloudfront.net";
readonly CLOUDFRONT_IMG: "https://d2k5nsl2zxldvw.cloudfront.net/images/";
readonly CLOUDFRONT_FILES: "https://d2k5nsl2zxldvw.cloudfront.net/files/";
readonly EXPENSIFY_SYNC_MANAGER: "quickbooksdesktop/Expensify_QuickBooksDesktop_Setup_230403124.exe";
readonly EXPENSIFY_SYNC_MANAGER: "quickbooksdesktop/Expensify_QuickBooksDesktop_Setup_2300802.exe";
readonly USEDOT_ROOT: "https://use.expensify.com/";
readonly ITUNES_SUBSCRIPTION: "https://buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/manageSubscriptions";
};
Expand Down Expand Up @@ -436,7 +436,7 @@ export declare const CONST: {
readonly REPORT_SUBMITTED: "REPORT_SUBMITTED";
};
readonly XERO_HQ_CONNECTION_NAME: "xerohq";
readonly EXPENSIFY_SYNC_MANAGER_VERSION: "23.0.403.124";
readonly EXPENSIFY_SYNC_MANAGER_VERSION: "23.0.802.0";
};
readonly INTEGRATION_TYPES: {
readonly ACCOUNTING: "accounting";
Expand Down
18 changes: 16 additions & 2 deletions lib/CONST.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ export const CONST = {
*/
MILEAGE_IRS_RATE: (new Date() > new Date(2019, 1, 1)) ? 0.545 : 0.58,

/**
* Display this amount to users to encourage them to book a call
*
* @type Number
*/
MAX_TRIAL_BONUS_DAYS: 42,

COUNTRY: {
US: 'US',
AU: 'AU',
Expand Down Expand Up @@ -263,6 +270,13 @@ export const CONST = {
}
},

/**
* Special characters that need to be removed when they are ending an url
*
* @type String
*/
SPECIAL_CHARS_TO_REMOVE: '$*.+!(,=',

/**
* Store all the regular expression we are using for matching stuff
*/
Expand Down Expand Up @@ -376,7 +390,7 @@ export const CONST = {
CLOUDFRONT: 'https://d2k5nsl2zxldvw.cloudfront.net',
CLOUDFRONT_IMG: 'https://d2k5nsl2zxldvw.cloudfront.net/images/',
CLOUDFRONT_FILES: 'https://d2k5nsl2zxldvw.cloudfront.net/files/',
EXPENSIFY_SYNC_MANAGER: 'quickbooksdesktop/Expensify_QuickBooksDesktop_Setup_230403124.exe',
EXPENSIFY_SYNC_MANAGER: 'quickbooksdesktop/Expensify_QuickBooksDesktop_Setup_2300802.exe',
USEDOT_ROOT: 'https://use.expensify.com/',
ITUNES_SUBSCRIPTION: 'https://buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/manageSubscriptions'
},
Expand Down Expand Up @@ -567,7 +581,7 @@ export const CONST = {

XERO_HQ_CONNECTION_NAME: 'xerohq',

EXPENSIFY_SYNC_MANAGER_VERSION: '23.0.403.124',
EXPENSIFY_SYNC_MANAGER_VERSION: '23.0.802.0',
},

INTEGRATION_TYPES: {
Expand Down
103 changes: 72 additions & 31 deletions lib/ExpensiMark.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,12 @@ export default class ExpensiMark {
);
return this.modifyTextForEmailLinks(regex, textToProcess, replacement);
},
replacement: (match, g1, g2) => `<a href="mailto:${g2}">${g1.trim()}</a>`,
replacement: (match, g1, g2) => {
if (g1.match(CONST.REG_EXP.EMOJIS) || !g1.trim()) {
return match;
}
return `<a href="mailto:${g2}">${g1.trim()}</a>`;
},
},

/**
Expand Down Expand Up @@ -210,8 +215,8 @@ export default class ExpensiMark {
},
{
name: 'strikethrough',
regex: /\B~((?=\S)((~~(?!~)|[^\s~]|\s(?!~))+?))~\B(?![^<]*(<\/pre>|<\/code>|<\/a>))/g,
replacement: (match, g1) => (this.containsNonPairTag(g1) ? match : `<del>${g1}</del>`),
regex: /\B~((?![\s~])[\s\S]*?[^\s~])~\B(?![^<]*(<\/pre>|<\/code>|<\/a>))/g,
replacement: (match, g1) => (g1.includes('<pre>') || this.containsNonPairTag(g1) ? match : `<del>${g1}</del>`),
},
{
name: 'newline',
Expand Down Expand Up @@ -282,7 +287,7 @@ export default class ExpensiMark {
},
{
name: 'heading1',
regex: /[^\S\r\n]*<(h1)(?:"[^"]*"|'[^']*'|[^'">])*>(.*?)<\/\1>(?![^<]*(<\/pre>|<\/code>))[^\S\r\n]*/gi,
regex: /[^\S\r\n]*<(h1)(?:"[^"]*"|'[^']*'|[^'">])*>(.*?)<\/\1>(?![^<]*(<\/pre>|<\/code>))/gi,
replacement: '<h1># $2</h1>',
},
{
Expand Down Expand Up @@ -446,39 +451,72 @@ export default class ExpensiMark {
let startIndex = 0;

while (match !== null) {
// we want to avoid matching email address domains
let abort = false;
if ((match.index !== 0) && (textToCheck[match.index - 1] === '@')) {
abort = true;
// We end the link at the last closing parenthesis that matches an opening parenthesis because unmatched closing parentheses are unlikely to be in the url
// and can be part of markdown for example
let unmatchedOpenParentheses = 0;
let url = match[2];
for (let i = 0; i < url.length; i++) {
if (url[i] === '(') {
unmatchedOpenParentheses++;
} else if (url[i] === ')') {
// Unmatched closing parenthesis
if (unmatchedOpenParentheses <= 0) {
const numberOfCharsToRemove = url.length - i;
match[0] = match[0].substr(0, match[0].length - numberOfCharsToRemove);
url = url.substr(0, url.length - numberOfCharsToRemove);
break;
}
unmatchedOpenParentheses--;
}
}

// we want to avoid matching ending ) unless it is a closing parenthesis for the URL
if (textToCheck[(match.index + match[2].length) - 1] === ')' && !match[2].includes('(')) {
match[0] = match[0].substr(0, match[0].length - 1);
match[2] = match[2].substr(0, match[2].length - 1);
}
// Because we are removing ) parenthesis, some special characters that shouldn't be in the href are in the href
// For example google.com/toto.) is accepted by the regular expression above and we remove the ) parenthesis, so the link becomes google.com/toto. which is not a valid link
// In that case we should also remove the "."
// Those characters should only be remove from the url if this url doesn't have a parameter or a fragment
if (!url.includes('?') && !url.includes('#')) {
let numberOfCharsToRemove = 0;

// remove extra ) parentheses
let brace = 0;
if (match[2][match[2].length - 1] === ')') {
for (let i = 0; i < match[2].length; i++) {
if (match[2][i] === '(') {
brace++;
}
if (match[2][i] === ')') {
brace--;
for (let i = url.length - 1; i >= 0; i--) {
if (CONST.SPECIAL_CHARS_TO_REMOVE.includes(url[i])) {
numberOfCharsToRemove++;
} else {
break;
}
}
if (brace) {
match[0] = match[0].substr(0, match[0].length + brace);
match[2] = match[2].substr(0, match[2].length + brace);
if (numberOfCharsToRemove) {
match[0] = match[0].substring(0, match[0].length - numberOfCharsToRemove);
url = url.substring(0, url.length - numberOfCharsToRemove);
}
}

replacedText = replacedText.concat(textToCheck.substr(startIndex, (match.index - startIndex)));

if (abort || match[1].includes('<pre>')) {
// We want to avoid matching domains in email addresses so we don't render them as URLs,
// but we need to check if there are valid URLs after the email address and render them accordingly,
// e.g. [email protected]/https://www.test.com
let isDoneMatching = false;
let shouldApplyAutoLinkAgain = true;

// If we find a URL with a leading @ sign, we need look for other domains in the rest of the string
if ((match.index !== 0) && (textToCheck[match.index - 1] === '@')) {
const domainRegex = new RegExp('^(([a-z-0-9]+\\.)+[a-z]{2,})(\\S*)', 'i');
const domainMatch = domainRegex.exec(url);

// If we find another domain in the remainder of the string, we apply the auto link rule again and set a flag to avoid re-doing below.
if ((domainMatch !== null) && (domainMatch[3] !== '')) {
replacedText = replacedText.concat(domainMatch[1] + this.replace(domainMatch[3], {filterRules: ['autolink']}));
shouldApplyAutoLinkAgain = false;
} else {
// Otherwise, we're done applying rules
isDoneMatching = true;
}
}

// We don't want to apply link rule if match[1] contains the code block inside the [] of the markdown e.g. [```example```](https://example.com)
if (isDoneMatching || match[1].includes('<pre>')) {
replacedText = replacedText.concat(textToCheck.substr(match.index, (match[0].length)));
} else {
} else if (shouldApplyAutoLinkAgain) {
const urlRegex = new RegExp(`^${LOOSE_URL_REGEX}$|^${URL_REGEX}$`, 'i');

// `match[1]` contains the text inside the [] of the markdown e.g. [example](https://example.com)
Expand All @@ -490,7 +528,7 @@ export default class ExpensiMark {
filterRules: ['bold', 'strikethrough', 'italic'],
shouldEscapeText: false,
});
replacedText = replacedText.concat(replacement(match[0], linkText, match[2]));
replacedText = replacedText.concat(replacement(match[0], linkText, url));
}
startIndex = match.index + (match[0].length);

Expand Down Expand Up @@ -754,11 +792,14 @@ export default class ExpensiMark {
*/
extractLinksInMarkdownComment(comment) {
try {
const escapedComment = _.escape(comment);
const matches = [...escapedComment.matchAll(MARKDOWN_LINK_REGEX)];
const htmlString = this.replace(comment, {filterRules: ['link']});

// We use same anchor tag template as link and autolink rules to extract link
const regex = new RegExp(`<a href="${MARKDOWN_URL_REGEX}" target="_blank" rel="noreferrer noopener">`, 'gi');
const matches = [...htmlString.matchAll(regex)];

// Element 1 from match is the regex group if it exists which contains the link URLs
const links = _.map(matches, match => Str.sanitizeURL(match[2]));
const links = _.map(matches, match => Str.sanitizeURL(match[1]));
return links;
} catch (e) {
// eslint-disable-next-line no-console
Expand Down
2 changes: 1 addition & 1 deletion lib/Logger.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
declare type Parameters = string | Record<string, unknown>;
declare type Parameters = string | Record<string, unknown> | Array<Record<string, unknown>>;
declare type ServerLoggingCallbackOptions = {api_setCookie: boolean; logPacket: string};
declare type ServerLoggingCallback = (logger: Logger, options: ServerLoggingCallbackOptions) => Promise<{requestID: string}> | undefined;
declare type ClientLoggingCallBack = (message: string) => void;
Expand Down
2 changes: 1 addition & 1 deletion lib/Url.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import TLD_REGEX from './tlds';
const ALLOWED_PORTS = '([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])';
const URL_PROTOCOL_REGEX = '((ht|f)tps?:\\/\\/)';
const URL_WEBSITE_REGEX = `${URL_PROTOCOL_REGEX}?((?:www\\.)?[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\\.)+(?:${TLD_REGEX})(?:\\:${ALLOWED_PORTS}|\\b|(?=_))(?!@(?:[a-z\\d-]+\\.)+[a-z]{2,})`;
const addEscapedChar = reg => `(?:${reg}|&(?:amp|quot|#x27);)`;
const addEscapedChar = reg => `(?:${reg}|&(?:amp|#x27);)`;
const URL_PATH_REGEX = `(?:${addEscapedChar('[.,=(+$!*]')}?\\/${addEscapedChar('[-\\w$@.+!*:(),=%~]')}*${addEscapedChar('[-\\w~@:%)]')}|\\/)*`;
const URL_PARAM_REGEX = `(?:\\?${addEscapedChar('[-\\w$@.+!*()\\/,=%{}:;\\[\\]\\|_|~]')}*)?`;
const URL_FRAGMENT_REGEX = `(?:#${addEscapedChar('[-\\w$@.+!*()[\\],=%;\\/:~]')}*)?`;
Expand Down

0 comments on commit 29f3255

Please sign in to comment.