From 0c5bc1ae5101180362b21e4ee413c8ee56e6c986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Thu, 23 May 2024 14:24:43 +0200 Subject: [PATCH 01/33] Remmove undersore from ExpensiMark --- lib/ExpensiMark.js | 58 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/lib/ExpensiMark.js b/lib/ExpensiMark.js index c1f437ff..b81404cb 100644 --- a/lib/ExpensiMark.js +++ b/lib/ExpensiMark.js @@ -1,4 +1,4 @@ -import * as _ from 'underscore'; +/* eslint-disable rulesdir/prefer-underscore-method */ import Str from './str'; import * as Constants from './CONST'; import * as UrlPatterns from './Url'; @@ -451,7 +451,7 @@ export default class ExpensiMark { } // Use _.map with the named function - resultString = _.map(resultString, wrapWithBlockquote); + resultString = resultString.map(wrapWithBlockquote); function processString(m) { // Recursive function to replace nested
with ">" @@ -468,7 +468,7 @@ export default class ExpensiMark { return replaceBlockquotes(m); } - resultString = _.map(resultString, processString).join('\n'); + resultString = resultString.map(processString).join('\n'); // We want to keep
tag here and let method replaceBlockElementWithNewLine to handle the line break later return `
${resultString}
`; @@ -621,13 +621,13 @@ export default class ExpensiMark { * @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); + this.filterRules = (rule) => !this.whitespaceRulesToDisable.includes(rule.name); /** * Filters rules to determine which should keep whitespace. * @returns {Object[]} The filtered rules. */ - this.shouldKeepWhitespaceRules = _.filter(this.rules, this.filterRules); + this.shouldKeepWhitespaceRules = this.rules.filter(this.filterRules); /** * maxQuoteDepth is the maximum depth of nested quotes that we want to support. @@ -642,18 +642,40 @@ export default class ExpensiMark { this.currentQuoteDepth = 0; } + escape(text) { + const matchHtmlRegExp = /["'&<>`]/g; + return text.replace(matchHtmlRegExp, (match) => { + switch (match) { + case '&': + return '&'; + case '<': + return '<'; + case '>': + return '>'; + case '"': + return '"'; + case "'": + return '''; + case '`': + return '`'; + default: + return match; + } + }); + } + getHtmlRuleset(filterRules, disabledRules, shouldKeepRawInput) { let rules = this.rules; - const hasRuleName = (rule) => _.contains(filterRules, rule.name); - const hasDisabledRuleName = (rule) => !_.contains(disabledRules, rule.name); + const hasRuleName = (rule) => filterRules.includes(rule.name); + const hasDisabledRuleName = (rule) => !disabledRules.includes(rule.name); if (shouldKeepRawInput) { rules = this.shouldKeepWhitespaceRules; } - if (!_.isEmpty(filterRules)) { - rules = _.filter(this.rules, hasRuleName); + if (filterRules.length > 0) { + rules = this.rules.filter(hasRuleName); } - if (!_.isEmpty(disabledRules)) { - rules = _.filter(rules, hasDisabledRuleName); + if (disabledRules.length > 0) { + rules = rules.filter(hasDisabledRuleName); } return rules; } @@ -673,7 +695,7 @@ export default class ExpensiMark { */ replace(text, {filterRules = [], shouldEscapeText = true, shouldKeepRawInput = false, disabledRules = []} = {}) { // This ensures that any html the user puts into the comment field shows as raw html - let replacedText = shouldEscapeText ? _.escape(text) : text; + let replacedText = shouldEscapeText ? this.escape(text) : text; const rules = this.getHtmlRuleset(filterRules, disabledRules, shouldKeepRawInput); const processRule = (rule) => { @@ -700,7 +722,7 @@ export default class ExpensiMark { console.warn('Error replacing text with html in ExpensiMark.replace', {error: e}); // We want to return text without applying rules if exception occurs during replacing - return shouldEscapeText ? _.escape(text) : text; + return shouldEscapeText ? this.escape(text) : text; } return replacedText; @@ -870,7 +892,7 @@ export default class ExpensiMark { /|<\/div>||\n<\/comment>|<\/comment>|

|<\/h1>|

|<\/h2>|

|<\/h3>|

|<\/h4>|

|<\/h5>|
|<\/h6>|

|<\/p>|

  • |<\/li>|
    |<\/blockquote>/, ); const stripHTML = (text) => Str.stripHTML(text); - splitText = _.map(splitText, stripHTML); + splitText = splitText.map(stripHTML); let joinedText = ''; // Delete whitespace at the end @@ -1038,7 +1060,7 @@ export default class ExpensiMark { } return quoteContent; }; - let textToFormat = _.map(textToCheck.split('\n'), formatRow).join('\n'); + let textToFormat = textToCheck.split('\n').map(formatRow).join('\n'); // Remove leading and trailing line breaks textToFormat = textToFormat.replace(/^\n+|\n+$/g, ''); @@ -1099,7 +1121,7 @@ export default class ExpensiMark { // Element 1 from match is the regex group if it exists which contains the link URLs const sanitizeMatch = (match) => Str.sanitizeURL(match[1]); - const links = _.map(matches, sanitizeMatch); + const links = matches.map(sanitizeMatch); return links; } catch (e) { // eslint-disable-next-line no-console @@ -1118,7 +1140,7 @@ export default class ExpensiMark { getRemovedMarkdownLinks(oldComment, newComment) { const linksInOld = this.extractLinksInMarkdownComment(oldComment); const linksInNew = this.extractLinksInMarkdownComment(newComment); - return linksInOld === undefined || linksInNew === undefined ? [] : _.difference(linksInOld, linksInNew); + return linksInOld === undefined || linksInNew === undefined ? [] : linksInOld.filter((link) => !linksInNew.includes(link)); } /** @@ -1135,6 +1157,6 @@ export default class ExpensiMark { // When the attribute contains HTML and is converted back to MD we need to re-escape it to avoid // illegal attribute value characters like `," or ' which might break the HTML originalContent = Str.replaceAll(originalContent, '\n', ''); - return _.escape(originalContent); + return this.escape(originalContent); } } From a5644393acc137a883acdc17a206f5535a61f9ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Thu, 23 May 2024 14:49:42 +0200 Subject: [PATCH 02/33] Move escape & unescape to utils file --- lib/ExpensiMark.js | 29 ++++------------------------- lib/utils.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 25 deletions(-) create mode 100644 lib/utils.js diff --git a/lib/ExpensiMark.js b/lib/ExpensiMark.js index b81404cb..6568e038 100644 --- a/lib/ExpensiMark.js +++ b/lib/ExpensiMark.js @@ -3,6 +3,7 @@ import Str from './str'; import * as Constants from './CONST'; import * as UrlPatterns from './Url'; import Logger from './Logger'; +import * as Utils from './utils'; const MARKDOWN_LINK_REGEX = new RegExp(`\\[([^\\][]*(?:\\[[^\\][]*][^\\][]*)*)]\\(${UrlPatterns.MARKDOWN_URL_REGEX}\\)(?![^<]*(<\\/pre>|<\\/code>))`, 'gi'); const MARKDOWN_IMAGE_REGEX = new RegExp(`\\!(?:\\[([^\\][]*(?:\\[[^\\][]*][^\\][]*)*)])?\\(${UrlPatterns.MARKDOWN_URL_REGEX}\\)(?![^<]*(<\\/pre>|<\\/code>))`, 'gi'); @@ -642,28 +643,6 @@ export default class ExpensiMark { this.currentQuoteDepth = 0; } - escape(text) { - const matchHtmlRegExp = /["'&<>`]/g; - return text.replace(matchHtmlRegExp, (match) => { - switch (match) { - case '&': - return '&'; - case '<': - return '<'; - case '>': - return '>'; - case '"': - return '"'; - case "'": - return '''; - case '`': - return '`'; - default: - return match; - } - }); - } - getHtmlRuleset(filterRules, disabledRules, shouldKeepRawInput) { let rules = this.rules; const hasRuleName = (rule) => filterRules.includes(rule.name); @@ -695,7 +674,7 @@ export default class ExpensiMark { */ replace(text, {filterRules = [], shouldEscapeText = true, shouldKeepRawInput = false, disabledRules = []} = {}) { // This ensures that any html the user puts into the comment field shows as raw html - let replacedText = shouldEscapeText ? this.escape(text) : text; + let replacedText = shouldEscapeText ? Utils.escape(text) : text; const rules = this.getHtmlRuleset(filterRules, disabledRules, shouldKeepRawInput); const processRule = (rule) => { @@ -722,7 +701,7 @@ export default class ExpensiMark { console.warn('Error replacing text with html in ExpensiMark.replace', {error: e}); // We want to return text without applying rules if exception occurs during replacing - return shouldEscapeText ? this.escape(text) : text; + return shouldEscapeText ? Utils.escape(text) : text; } return replacedText; @@ -1157,6 +1136,6 @@ export default class ExpensiMark { // When the attribute contains HTML and is converted back to MD we need to re-escape it to avoid // illegal attribute value characters like `," or ' which might break the HTML originalContent = Str.replaceAll(originalContent, '\n', ''); - return this.escape(originalContent); + return Utils.escape(originalContent); } } diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 00000000..bb39484c --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,44 @@ +const htmlEscapes = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '`': '`', +}; +const reUnescapedHtml = /[&<>"'`]/g; +const reHasUnescapedHtml = RegExp(reUnescapedHtml.source); + +/** + * Converts the characters "&", "<", ">", '"', and "'" in `string` to their + * corresponding HTML entities. + * @param {string} string - The input string to escape. + * @returns {string} - The escaped string. + */ +function escape(string) { + return string && reHasUnescapedHtml.test(string) ? string.replace(reUnescapedHtml, (chr) => htmlEscapes[chr]) : string || ''; +} + +const htmlUnescapes = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ''': "'", +}; + +const reEscapedHtml = /&(?:amp|lt|gt|quot|#(0+)?39);/g; +const reHasEscapedHtml = RegExp(reEscapedHtml.source); + +/** + * The inverse of `escape`this method converts the HTML entities + * `&`, `<`, `>`, `"` and `'` in `string` to + * their corresponding characters. + * @param {string} string - The input string to unescape. + * @returns {string} - The unescaped string. + * */ +function unescape(string) { + return string && reHasEscapedHtml.test(string) ? string.replace(reEscapedHtml, (entity) => htmlUnescapes[entity] || "'") : string || ''; +} + +export {escape, unescape}; From 269d6af3b77c32881e5353523372c827be888ac4 Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Thu, 23 May 2024 15:13:58 +0200 Subject: [PATCH 03/33] remove jQuery --- lib/str.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/lib/str.ts b/lib/str.ts index 0e9da4a4..14018372 100644 --- a/lib/str.ts +++ b/lib/str.ts @@ -5,6 +5,7 @@ import {parsePhoneNumber} from 'awesome-phonenumber'; import * as HtmlEntities from 'html-entities'; import * as Constants from './CONST'; import * as UrlPatterns from './Url'; +import * as Utils from './utils'; const REMOVE_SMS_DOMAIN_PATTERN = /@expensify\.sms/gi; @@ -87,11 +88,7 @@ const Str = { * @param s The string to decode. * @returns The decoded string. */ - htmlDecode(s: string): string { - // Use jQuery if it exists or else use html-entities - if (typeof jQuery !== 'undefined') { - return jQuery('