Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main' into #41696-quotes-with-…
Browse files Browse the repository at this point in the history
…space
  • Loading branch information
robertKozik committed May 7, 2024
2 parents df28ecb + 182ef7f commit 831e205
Show file tree
Hide file tree
Showing 11 changed files with 292 additions and 227 deletions.
8 changes: 4 additions & 4 deletions lib/CONST.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,11 +254,11 @@ export declare const CONST: {
/**
* Regex matching a text containing general phone number
*/
readonly GENERAL_PHONE_PART: RegExp,
readonly GENERAL_PHONE_PART: RegExp;
/**
* Regex matching a text containing an E.164 format phone number
*/
readonly PHONE_PART: "\\+[1-9]\\d{1,14}";
* Regex matching a text containing an E.164 format phone number
*/
readonly PHONE_PART: '\\+[1-9]\\d{1,14}';
/**
* Regular expression to check that a basic name is valid
*/
Expand Down
15 changes: 8 additions & 7 deletions lib/CONST.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -291,15 +291,15 @@ export const CONST = {
EMAIL_PART: EMAIL_BASE_REGEX,

/**
* Regex matching a text containing general phone number
*
* @type RegExp
*/
* Regex matching a text containing general phone number
*
* @type RegExp
*/
GENERAL_PHONE_PART: /^(\+\d{1,2}\s?)?(\(\d{3}\)|\d{3})[\s.-]?\d{3}[\s.-]?\d{4}$/,

/**
* Regex matching a text containing an E.164 format phone number
*/
* Regex matching a text containing an E.164 format phone number
*/
PHONE_PART: '\\+[1-9]\\d{1,14}',

/**
Expand Down Expand Up @@ -368,7 +368,8 @@ export const CONST = {
*
* @type RegExp
*/
EMOJI_RULE: /[\p{Extended_Pictographic}](\u200D[\p{Extended_Pictographic}]|[\u{1F3FB}-\u{1F3FF}]|[\u{E0020}-\u{E007F}]|\uFE0F|\u20E3)*|[\u{1F1E6}-\u{1F1FF}]{2}|[#*0-9]\uFE0F?\u20E3/gu,
EMOJI_RULE:
/[\p{Extended_Pictographic}](\u200D[\p{Extended_Pictographic}]|[\u{1F3FB}-\u{1F3FF}]|[\u{E0020}-\u{E007F}]|\uFE0F|\u20E3)*|[\u{1F1E6}-\u{1F1FF}]{2}|[#*0-9]\uFE0F?\u20E3/gu,
},

REPORT: {
Expand Down
121 changes: 70 additions & 51 deletions lib/ExpensiMark.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default class ExpensiMark {
{
name: 'emoji',
regex: Constants.CONST.REG_EXP.EMOJI_RULE,
replacement: match => `<emoji>${match}</emoji>`
replacement: (match) => `<emoji>${match}</emoji>`,
},

/**
Expand Down Expand Up @@ -124,7 +124,8 @@ export default class ExpensiMark {
name: 'image',
regex: MARKDOWN_IMAGE_REGEX,
replacement: (match, g1, g2) => `<img src="${Str.sanitizeURL(g2)}"${g1 ? ` alt="${this.escapeAttributeContent(g1)}"` : ''} />`,
rawInputReplacement: (match, g1, g2) => `<img src="${Str.sanitizeURL(g2)}"${g1 ? ` alt="${this.escapeAttributeContent(g1)}"` : ''} data-raw-href="${g2}" data-link-variant="${typeof (g1) === 'string' ? 'labeled' : 'auto'}" />`
rawInputReplacement: (match, g1, g2) =>
`<img src="${Str.sanitizeURL(g2)}"${g1 ? ` alt="${this.escapeAttributeContent(g1)}"` : ''} data-raw-href="${g2}" data-link-variant="${typeof g1 === 'string' ? 'labeled' : 'auto'}" />`,
},

/**
Expand Down Expand Up @@ -191,7 +192,10 @@ export default class ExpensiMark {
*/
{
name: 'userMentions',
regex: new RegExp(`(@here|[a-zA-Z0-9.!$%&+=?^\`{|}-]?)(@${Constants.CONST.REG_EXP.EMAIL_PART}|@${Constants.CONST.REG_EXP.PHONE_PART})(?!((?:(?!<a).)+)?<\\/a>|[^<]*(<\\/pre>|<\\/code>))`, 'gim'),
regex: new RegExp(
`(@here|[a-zA-Z0-9.!$%&+=?^\`{|}-]?)(@${Constants.CONST.REG_EXP.EMAIL_PART}|@${Constants.CONST.REG_EXP.PHONE_PART})(?!((?:(?!<a).)+)?<\\/a>|[^<]*(<\\/pre>|<\\/code>))`,
'gim',
),
replacement: (match, g1, g2) => {
if (!Str.isValidMention(match)) {
return match;
Expand Down Expand Up @@ -221,10 +225,7 @@ export default class ExpensiMark {
name: 'autolink',

process: (textToProcess, replacement) => {
const regex = new RegExp(
`(?![^<]*>|[^<>]*<\\/(?!h1>))([_*~]*?)${UrlPatterns.MARKDOWN_URL_REGEX}\\1(?!((?:(?!<a).)+)?<\\/a>|[^<]*(<\\/pre>|<\\/code>|.+\\/>))`,
'gi',
);
const regex = new RegExp(`(?![^<]*>|[^<>]*<\\/(?!h1>))([_*~]*?)${UrlPatterns.MARKDOWN_URL_REGEX}\\1(?!((?:(?!<a).)+)?<\\/a>|[^<]*(<\\/pre>|<\\/code>|.+\\/>))`, 'gi');
return this.modifyTextForUrlLinks(regex, textToProcess, replacement);
},

Expand All @@ -235,7 +236,7 @@ export default class ExpensiMark {
rawInputReplacement: (_match, g1, g2) => {
const href = Str.sanitizeURL(g2);
return `${g1}<a href="${href}" data-raw-href="${g2}" data-link-variant="auto" target="_blank" rel="noreferrer noopener">${g2}</a>${g1}`;
}
},
},

{
Expand All @@ -246,22 +247,24 @@ export default class ExpensiMark {
// inline code blocks. A single prepending space should be stripped if it exists
process: (textToProcess, replacement, shouldKeepRawInput = false) => {
const regex = /^&gt;[ &gt;]+(?! )(?![^<]*(?:<\/pre>|<\/code>))([^\v\n\r]+)/gm;
const replaceFunction = (g1) => replacement(g1, shouldKeepRawInput);
if (shouldKeepRawInput) {
return textToProcess.replace(regex, (g1) => replacement(g1, shouldKeepRawInput));
return textToProcess.replace(regex, replaceFunction);
}
return this.modifyTextForQuote(regex, textToProcess, replacement);
},
replacement: (g1, shouldKeepRawInput = false) => {
// We want to enable 2 options of nested heading inside the blockquote: "># heading" and "> # heading".
// To do this we need to parse body of the quote without first space
let isStartingWithSpace = false;
const textToReplace = g1.replace(/^&gt;( )?/gm, (match, g2) => {
const handleMatch = (match, g2) => {
if (shouldKeepRawInput) {
isStartingWithSpace = !!g2;
return '';
}
return match;
});
};
const textToReplace = g1.replace(/^&gt;( )?/gm, handleMatch);
const filterRules = ['heading1'];

// if we don't reach the max quote depth we allow the recursive call to process possible quote
Expand All @@ -273,7 +276,7 @@ export default class ExpensiMark {
const replacedText = this.replace(textToReplace, {
filterRules,
shouldEscapeText: false,
shouldKeepRawInput
shouldKeepRawInput,
});
this.currentQuoteDepth = 0;
return `<blockquote>${isStartingWithSpace ? ' ' : ''}${replacedText}</blockquote>`;
Expand Down Expand Up @@ -426,8 +429,8 @@ export default class ExpensiMark {
.trim()
.split('\n');

resultString = _.map(resultString, (m) => `> ${m}`).join('\n');

const prependGreaterSign = (m) => `> ${m}`;
resultString = _.map(resultString, prependGreaterSign).join('\n');
// We want to keep <blockquote> tag here and let method replaceBlockElementWithNewLine to handle the line break later
return `<blockquote>${resultString}</blockquote>`;
},
Expand Down Expand Up @@ -462,7 +465,7 @@ export default class ExpensiMark {
}

return `!(${g2})`;
}
},
},
{
name: 'reportMentions',
Expand Down Expand Up @@ -559,7 +562,7 @@ export default class ExpensiMark {
name: 'stripTag',
regex: /(<([^>]+)>)/gi,
replacement: '',
}
},
];

/**
Expand All @@ -570,9 +573,16 @@ export default class ExpensiMark {

/**
* The list of rules that have to be applied when shouldKeepWhitespace flag is true.
* @type {Object[]}
* @param {Object} rule - The rule to check.
* @returns {boolean} Returns true if the rule should be applied, otherwise false.
*/
this.filterRules = (rule) => !_.includes(this.whitespaceRulesToDisable, rule.name);

/**
* Filters rules to determine which should keep whitespace.
* @returns {Object[]} The filtered rules.
*/
this.shouldKeepWhitespaceRules = _.filter(this.rules, (rule) => !_.includes(this.whitespaceRulesToDisable, rule.name));
this.shouldKeepWhitespaceRules = _.filter(this.rules, this.filterRules);

/**
* maxQuoteDepth is the maximum depth of nested quotes that we want to support.
Expand All @@ -589,14 +599,16 @@ export default class ExpensiMark {

getHtmlRuleset(filterRules, disabledRules, shouldKeepRawInput) {
let rules = this.rules;
if(shouldKeepRawInput) {
const hasRuleName = (rule) => _.contains(filterRules, rule.name);
const hasDisabledRuleName = (rule) => !_.contains(disabledRules, rule.name);
if (shouldKeepRawInput) {
rules = this.shouldKeepWhitespaceRules;
}
if (!_.isEmpty(filterRules)) {
rules = _.filter(this.rules, (rule) => _.contains(filterRules, rule.name));
rules = _.filter(this.rules, hasRuleName);
}
if (!_.isEmpty(disabledRules)) {
rules = _.filter(rules, (rule) => !_.contains(disabledRules, rule.name));
rules = _.filter(rules, hasDisabledRuleName);
}
return rules;
}
Expand All @@ -619,24 +631,25 @@ export default class ExpensiMark {
let replacedText = shouldEscapeText ? _.escape(text) : text;
const rules = this.getHtmlRuleset(filterRules, disabledRules, shouldKeepRawInput);

try {
rules.forEach((rule) => {
// Pre-process text before applying regex
if (rule.pre) {
replacedText = rule.pre(replacedText);
}
const replacementFunction = shouldKeepRawInput && rule.rawInputReplacement ? rule.rawInputReplacement : rule.replacement;
if (rule.process) {
replacedText = rule.process(replacedText, replacementFunction, shouldKeepRawInput);
} else {
replacedText = replacedText.replace(rule.regex, replacementFunction);
}
const processRule = (rule) => {
// Pre-process text before applying regex
if (rule.pre) {
replacedText = rule.pre(replacedText);
}
const replacementFunction = shouldKeepRawInput && rule.rawInputReplacement ? rule.rawInputReplacement : rule.replacement;
if (rule.process) {
replacedText = rule.process(replacedText, replacementFunction, shouldKeepRawInput);
} else {
replacedText = replacedText.replace(rule.regex, replacementFunction);
}

// Post-process text after applying regex
if (rule.post) {
replacedText = rule.post(replacedText);
}
});
// Post-process text after applying regex
if (rule.post) {
replacedText = rule.post(replacedText);
}
};
try {
rules.forEach(processRule);
} catch (e) {
// eslint-disable-next-line no-console
console.warn('Error replacing text with html in ExpensiMark.replace', {error: e});
Expand Down Expand Up @@ -811,7 +824,8 @@ export default class ExpensiMark {
let splitText = htmlString.split(
/<div.*?>|<\/div>|<comment.*?>|\n<\/comment>|<\/comment>|<h1>|<\/h1>|<h2>|<\/h2>|<h3>|<\/h3>|<h4>|<\/h4>|<h5>|<\/h5>|<h6>|<\/h6>|<p>|<\/p>|<li>|<\/li>|<blockquote>|<\/blockquote>/,
);
splitText = _.map(splitText, (text) => Str.stripHTML(text));
const stripHTML = (text) => Str.stripHTML(text);
splitText = _.map(splitText, stripHTML);
let joinedText = '';

// Delete whitespace at the end
Expand All @@ -822,7 +836,7 @@ export default class ExpensiMark {
splitText.pop();
}

splitText.forEach((text, index) => {
const processText = (text, index) => {
if (text.trim().length === 0 && !text.match(/\n/)) {
return;
}
Expand All @@ -833,7 +847,9 @@ export default class ExpensiMark {
} else {
joinedText += `${text}\n`;
}
});
};

splitText.forEach(processText);

return joinedText;
}
Expand All @@ -856,7 +872,7 @@ export default class ExpensiMark {
generatedMarkdown = parseBodyTag[2];
}

this.htmlToMarkdownRules.forEach((rule) => {
const processRule = (rule) => {
// Pre-processes input HTML before applying regex
if (rule.pre) {
generatedMarkdown = rule.pre(generatedMarkdown);
Expand All @@ -865,7 +881,9 @@ export default class ExpensiMark {
// if replacement is a function, we want to pass optional extras to it
const replacementFunction = typeof rule.replacement === 'function' ? (...args) => rule.replacement(...args, extras) : rule.replacement;
generatedMarkdown = generatedMarkdown.replace(rule.regex, replacementFunction);
});
};

this.htmlToMarkdownRules.forEach(processRule);
return Str.htmlDecode(this.replaceBlockElementWithNewLine(generatedMarkdown));
}

Expand All @@ -879,13 +897,13 @@ export default class ExpensiMark {
*/
htmlToText(htmlString, extras) {
let replacedText = htmlString;

this.htmlToTextRules.forEach((rule) => {

const processRule = (rule) => {
// if replacement is a function, we want to pass optional extras to it
const replacementFunction = typeof rule.replacement === 'function' ? (...args) => rule.replacement(...args, extras) : rule.replacement;
replacedText = replacedText.replace(rule.regex, replacementFunction);
});
};

this.htmlToTextRules.forEach(processRule);

// Unescaping because the text is escaped in 'replace' function
// We use 'htmlDecode' instead of 'unescape' to replace entities like '&#32;'
Expand Down Expand Up @@ -968,13 +986,14 @@ export default class ExpensiMark {
formatTextForQuote(regex, textToCheck, replacement) {
if (textToCheck.match(regex)) {
// Remove '&gt;' and trim the spaces between nested quotes
let textToFormat = _.map(textToCheck.split('\n'), (row) => {
const formatRow = (row) => {
const quoteContent = row[4] === ' ' ? row.substr(5) : row.substr(4);
if (quoteContent.trimStart().startsWith('&gt;')) {
return quoteContent.trimStart();
}
return quoteContent;
}).join('\n');
};
let textToFormat = _.map(textToCheck.split('\n'), formatRow).join('\n');

// Remove leading and trailing line breaks
textToFormat = textToFormat.replace(/^\n+|\n+$/g, '');
Expand Down Expand Up @@ -1029,13 +1048,13 @@ export default class ExpensiMark {
extractLinksInMarkdownComment(comment) {
try {
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="${UrlPatterns.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[1]));
const sanitizeMatch = (match) => Str.sanitizeURL(match[1]);
const links = _.map(matches, sanitizeMatch);
return links;
} catch (e) {
// eslint-disable-next-line no-console
Expand Down
Loading

0 comments on commit 831e205

Please sign in to comment.