From 97c52cdde4abbb8d8e5b1f9a90d85ec90e3e260f Mon Sep 17 00:00:00 2001 From: Antasel Date: Wed, 6 Sep 2023 19:37:33 +0600 Subject: [PATCH 1/3] fix to parse deep link --- __tests__/ExpensiMark-HTML-test.js | 54 ++++++++++++++++++++++++++++++ lib/ExpensiMark.js | 27 +++++++++++---- 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/__tests__/ExpensiMark-HTML-test.js b/__tests__/ExpensiMark-HTML-test.js index a98e35aa..6adf8420 100644 --- a/__tests__/ExpensiMark-HTML-test.js +++ b/__tests__/ExpensiMark-HTML-test.js @@ -609,6 +609,60 @@ test('Test a url ending with a closing parentheses autolinks correctly', () => { expect(parser.replace(testString)).toBe(resultString); }); +test('Test urls autolinks correctly', () => { + let testString = 'test@expensify.com https://www.expensify.com\n' + + 'test@expensify.com-https://www.expensify.com\n' + + 'test@expensify.com/https://www.expensify.com\n' + + 'test@expensify.com?https://www.expensify.com\n' + + 'test@expensify.com>https://www.expensify.com\n' + + 'https://staging.new.expensify.com/details/test@expensify.com\n' + + 'staging.new.expensify.com/details\n\n' + + 'https://www.expensify.com?name=test&email=test@expensify.com\n' + + 'https://staging.new.expensify.com/details?login=testing@gmail.com\n' + + 'staging.new.expensify.com/details?login=testing@gmail.com\n' + + 'http://necolas.github.io/react-native-web/docs/?path=/docs/components-pressable--disabled\n' + + '-https://www.expensify.com /https://www.expensify.com @https://www.expensify.com\n' + + 'expensify.com -expensify.com @expensify.com\n' + + 'https//www.expensify.com\n' + + '//www.expensify.com?name=test&email=test@expensify.com\n' + + '//staging.new.expensify.com/details?login=testing@gmail.com\n' + + '/details?login=testing@gmail.com\n' + + '?name=test&email=test@expensify.com\n\n' + + 'example.com/https://www.expensify.com\n' + + 'test@gmail.com staging.new.expensify.com/details?login=testing@gmail.com&redirectUrl=https://google.com\n' + + 'test@gmail.com //staging.new.expensify.com/details?login=testing@gmail.com&redirectUrl=https://google.com\n' + + 'test@gmail.com-https://staging.new.expensify.com/details?login=testing@gmail.com&redirectUrl=https://google.com\n' + + 'test@gmail.com/https://example.com/google@email.com?email=asd@email.com\n' + + 'test@gmail.com/test@gmail.com/https://example.com/google@email.com?email=asd@email.com'; + + let resultString = 'test@expensify.com https://www.expensify.com
' + + 'test@expensify.com-https://www.expensify.com
' + + 'test@expensify.com/https://www.expensify.com
' + + 'test@expensify.com?https://www.expensify.com
' + + 'test@expensify.com>https://www.expensify.com
' + + 'https://staging.new.expensify.com/details/test@expensify.com
' + + 'staging.new.expensify.com/details

'+ + 'https://www.expensify.com?name=test&email=test@expensify.com
' + + 'https://staging.new.expensify.com/details?login=testing@gmail.com
' + + 'staging.new.expensify.com/details?login=testing@gmail.com
' + + 'http://necolas.github.io/react-native-web/docs/?path=/docs/components-pressable--disabled
' + + '-https://www.expensify.com /https://www.expensify.com @https://www.expensify.com
' + + 'expensify.com -expensify.com @expensify.com
' + + 'https//www.expensify.com
' + + '//www.expensify.com?name=test&email=test@expensify.com
' + + '//staging.new.expensify.com/details?login=testing@gmail.com
' + + '/details?login=testing@gmail.com
' + + '?name=test&email=test@expensify.com

' + + 'example.com/https://www.expensify.com
' + + 'test@gmail.com staging.new.expensify.com/details?login=testing@gmail.com&redirectUrl=https://google.com
' + + 'test@gmail.com //staging.new.expensify.com/details?login=testing@gmail.com&redirectUrl=https://google.com
' + + 'test@gmail.com-https://staging.new.expensify.com/details?login=testing@gmail.com&redirectUrl=https://google.com
' + + 'test@gmail.com/https://example.com/google@email.com?email=asd@email.com
' + + 'test@gmail.com/test@gmail.com/https://example.com/google@email.com?email=asd@email.com'; + + expect(parser.replace(testString)).toBe(resultString); +}); + test('Test markdown style email link with various styles', () => { const testString = 'Go to ~[Expensify](concierge@expensify.com)~ ' + '_[Expensify](concierge@expensify.com)_ ' diff --git a/lib/ExpensiMark.js b/lib/ExpensiMark.js index 0c34246f..255534bc 100644 --- a/lib/ExpensiMark.js +++ b/lib/ExpensiMark.js @@ -446,12 +446,6 @@ 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 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); @@ -476,9 +470,28 @@ export default class ExpensiMark { } replacedText = replacedText.concat(textToCheck.substr(startIndex, (match.index - startIndex))); + // we want to avoid matching email address domains + let abort = false; + let shouldRetryByAtSign = false; + + 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(match[2]); + + // e.g. test@expensify.com/https://www.test.com + // If the matched string faces @ sign before it, + // We will retry to apply autolink rule to the string(e.g. /https://www.test.com) except for domain(e.g. expensify.com) after @ sign. + if ((domainMatch !== null) && (domainMatch[3] !== '')) { + shouldRetryByAtSign = true; + replacedText = replacedText.concat(domainMatch[1] + this.replace(domainMatch[3], {filterRules: ['autolink']})); + } else { + abort = true; + } + } + if (abort || match[1].includes('
')) {
                 replacedText = replacedText.concat(textToCheck.substr(match.index, (match[0].length)));
-            } else {
+            } else if (!shouldRetryByAtSign) {
                 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)

From 8bc8591c1fed41a6452fe57267a245cbe9654129 Mon Sep 17 00:00:00 2001
From: Antasel 
Date: Thu, 7 Sep 2023 18:50:30 +0600
Subject: [PATCH 2/3] amendment of test cases and comments

---
 __tests__/ExpensiMark-HTML-test.js | 153 +++++++++++++++++++----------
 lib/ExpensiMark.js                 |  15 +--
 2 files changed, 110 insertions(+), 58 deletions(-)

diff --git a/__tests__/ExpensiMark-HTML-test.js b/__tests__/ExpensiMark-HTML-test.js
index 6adf8420..d118dc75 100644
--- a/__tests__/ExpensiMark-HTML-test.js
+++ b/__tests__/ExpensiMark-HTML-test.js
@@ -610,57 +610,108 @@ test('Test a url ending with a closing parentheses autolinks correctly', () => {
 });
 
 test('Test urls autolinks correctly', () => {
-    let testString = 'test@expensify.com https://www.expensify.com\n' +
-    'test@expensify.com-https://www.expensify.com\n' +
-    'test@expensify.com/https://www.expensify.com\n' +
-    'test@expensify.com?https://www.expensify.com\n' +
-    'test@expensify.com>https://www.expensify.com\n' +
-    'https://staging.new.expensify.com/details/test@expensify.com\n' +
-    'staging.new.expensify.com/details\n\n' +
-    'https://www.expensify.com?name=test&email=test@expensify.com\n' +
-    'https://staging.new.expensify.com/details?login=testing@gmail.com\n' +
-    'staging.new.expensify.com/details?login=testing@gmail.com\n' +
-    'http://necolas.github.io/react-native-web/docs/?path=/docs/components-pressable--disabled\n' +
-    '-https://www.expensify.com /https://www.expensify.com @https://www.expensify.com\n' +
-    'expensify.com -expensify.com @expensify.com\n' +
-    'https//www.expensify.com\n' +
-    '//www.expensify.com?name=test&email=test@expensify.com\n' +
-    '//staging.new.expensify.com/details?login=testing@gmail.com\n' +
-    '/details?login=testing@gmail.com\n' +
-    '?name=test&email=test@expensify.com\n\n' +
-    'example.com/https://www.expensify.com\n' +
-    'test@gmail.com staging.new.expensify.com/details?login=testing@gmail.com&redirectUrl=https://google.com\n' +
-    'test@gmail.com //staging.new.expensify.com/details?login=testing@gmail.com&redirectUrl=https://google.com\n' +
-    'test@gmail.com-https://staging.new.expensify.com/details?login=testing@gmail.com&redirectUrl=https://google.com\n' +
-    'test@gmail.com/https://example.com/google@email.com?email=asd@email.com\n' +
-    'test@gmail.com/test@gmail.com/https://example.com/google@email.com?email=asd@email.com';
-
-    let resultString = 'test@expensify.com https://www.expensify.com
' + - 'test@expensify.com-https://www.expensify.com
' + - 'test@expensify.com/https://www.expensify.com
' + - 'test@expensify.com?https://www.expensify.com
' + - 'test@expensify.com>https://www.expensify.com
' + - 'https://staging.new.expensify.com/details/test@expensify.com
' + - 'staging.new.expensify.com/details

'+ - 'https://www.expensify.com?name=test&email=test@expensify.com
' + - 'https://staging.new.expensify.com/details?login=testing@gmail.com
' + - 'staging.new.expensify.com/details?login=testing@gmail.com
' + - 'http://necolas.github.io/react-native-web/docs/?path=/docs/components-pressable--disabled
' + - '-https://www.expensify.com /https://www.expensify.com @https://www.expensify.com
' + - 'expensify.com -expensify.com @expensify.com
' + - 'https//www.expensify.com
' + - '//www.expensify.com?name=test&email=test@expensify.com
' + - '//staging.new.expensify.com/details?login=testing@gmail.com
' + - '/details?login=testing@gmail.com
' + - '?name=test&email=test@expensify.com

' + - 'example.com/https://www.expensify.com
' + - 'test@gmail.com staging.new.expensify.com/details?login=testing@gmail.com&redirectUrl=https://google.com
' + - 'test@gmail.com //staging.new.expensify.com/details?login=testing@gmail.com&redirectUrl=https://google.com
' + - 'test@gmail.com-https://staging.new.expensify.com/details?login=testing@gmail.com&redirectUrl=https://google.com
' + - 'test@gmail.com/https://example.com/google@email.com?email=asd@email.com
' + - 'test@gmail.com/test@gmail.com/https://example.com/google@email.com?email=asd@email.com'; - - expect(parser.replace(testString)).toBe(resultString); + const testCases = [ + { + testString: 'test@expensify.com https://www.expensify.com', + resultString: 'test@expensify.com https://www.expensify.com', + }, + { + testString: 'test@expensify.com-https://www.expensify.com', + resultString: 'test@expensify.com-https://www.expensify.com', + }, + { + testString: 'test@expensify.com/https://www.expensify.com', + resultString: 'test@expensify.com/https://www.expensify.com', + }, + { + testString: 'test@expensify.com?https://www.expensify.com', + resultString: 'test@expensify.com?https://www.expensify.com', + }, + { + testString: 'test@expensify.com>https://www.expensify.com', + resultString: 'test@expensify.com>https://www.expensify.com', + }, + { + testString: 'https://staging.new.expensify.com/details/test@expensify.com', + resultString: 'https://staging.new.expensify.com/details/test@expensify.com', + }, + { + testString: 'staging.new.expensify.com/details', + resultString: 'staging.new.expensify.com/details', + }, + { + testString: 'https://www.expensify.com?name=test&email=test@expensify.com', + resultString: 'https://www.expensify.com?name=test&email=test@expensify.com', + }, + { + testString: 'https://staging.new.expensify.com/details?login=testing@gmail.com', + resultString: 'https://staging.new.expensify.com/details?login=testing@gmail.com', + }, + { + testString: 'staging.new.expensify.com/details?login=testing@gmail.com', + resultString: 'staging.new.expensify.com/details?login=testing@gmail.com', + }, + { + testString: 'http://necolas.github.io/react-native-web/docs/?path=/docs/components-pressable--disabled', + resultString: 'http://necolas.github.io/react-native-web/docs/?path=/docs/components-pressable--disabled', + }, + { + testString: '-https://www.expensify.com /https://www.expensify.com @https://www.expensify.com', + resultString: '-https://www.expensify.com /https://www.expensify.com @https://www.expensify.com', + }, + { + testString: 'expensify.com -expensify.com @expensify.com', + resultString: 'expensify.com -expensify.com @expensify.com', + }, + { + testString: 'https//www.expensify.com', + resultString: 'https//www.expensify.com', + }, + { + testString: '//www.expensify.com?name=test&email=test@expensify.com', + resultString: '//www.expensify.com?name=test&email=test@expensify.com', + }, + { + testString: '//staging.new.expensify.com/details?login=testing@gmail.com', + resultString: '//staging.new.expensify.com/details?login=testing@gmail.com', + }, + { + testString: '/details?login=testing@gmail.com', + resultString: '/details?login=testing@gmail.com', + }, + { + testString: '?name=test&email=test@expensify.com', + resultString: '?name=test&email=test@expensify.com', + }, + { + testString: 'example.com/https://www.expensify.com', + resultString: 'example.com/https://www.expensify.com', + }, + { + testString: 'test@gmail.com staging.new.expensify.com/details?login=testing@gmail.com&redirectUrl=https://google.com', + resultString: 'test@gmail.com staging.new.expensify.com/details?login=testing@gmail.com&redirectUrl=https://google.com', + }, + { + testString: 'test@gmail.com //staging.new.expensify.com/details?login=testing@gmail.com&redirectUrl=https://google.com', + resultString: 'test@gmail.com //staging.new.expensify.com/details?login=testing@gmail.com&redirectUrl=https://google.com', + }, + { + testString: 'test@gmail.com-https://staging.new.expensify.com/details?login=testing@gmail.com&redirectUrl=https://google.com', + resultString: 'test@gmail.com-https://staging.new.expensify.com/details?login=testing@gmail.com&redirectUrl=https://google.com', + }, + { + testString: 'test@gmail.com/https://example.com/google@email.com?email=asd@email.com', + resultString: 'test@gmail.com/https://example.com/google@email.com?email=asd@email.com', + }, + { + testString: 'test@gmail.com/test@gmail.com/https://example.com/google@email.com?email=asd@email.com', + resultString: 'test@gmail.com/test@gmail.com/https://example.com/google@email.com?email=asd@email.com', + }, + ]; + + testCases.forEach(testCase => { + expect(parser.replace(testCase.testString)).toBe(testCase.resultString); + }); }); test('Test markdown style email link with various styles', () => { diff --git a/lib/ExpensiMark.js b/lib/ExpensiMark.js index 255534bc..9a2300b7 100644 --- a/lib/ExpensiMark.js +++ b/lib/ExpensiMark.js @@ -470,19 +470,20 @@ export default class ExpensiMark { } replacedText = replacedText.concat(textToCheck.substr(startIndex, (match.index - startIndex))); - // we want to avoid matching email address domains + // We want to avoid matching domains in email addresses so we don't render them as URLs. + // If the matched Url has a leading @ sign, The Url's domain can be a domain of email address. + // At this case, The matched string should not be parsed as Url, + // But it can contain another Url which should be parsed. (e.g. test@expensify.com/https://www.test.com) + // So We will retry to apply autolink rule to the string(e.g. /https://www.test.com) except for domain(e.g. expensify.com) after @ sign. let abort = false; - let shouldRetryByAtSign = false; + let isReparsedByAutolink = false; 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(match[2]); - // e.g. test@expensify.com/https://www.test.com - // If the matched string faces @ sign before it, - // We will retry to apply autolink rule to the string(e.g. /https://www.test.com) except for domain(e.g. expensify.com) after @ sign. if ((domainMatch !== null) && (domainMatch[3] !== '')) { - shouldRetryByAtSign = true; + isReparsedByAutolink = true; replacedText = replacedText.concat(domainMatch[1] + this.replace(domainMatch[3], {filterRules: ['autolink']})); } else { abort = true; @@ -491,7 +492,7 @@ export default class ExpensiMark { if (abort || match[1].includes('
')) {
                 replacedText = replacedText.concat(textToCheck.substr(match.index, (match[0].length)));
-            } else if (!shouldRetryByAtSign) {
+            } else if (!isReparsedByAutolink) {
                 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)

From bfa8f11c64da56d7eae20203fa685e6e83383374 Mon Sep 17 00:00:00 2001
From: Antasel 
Date: Sat, 9 Sep 2023 03:42:34 +0600
Subject: [PATCH 3/3] update comments and variable names

---
 lib/ExpensiMark.js | 24 +++++++++++++-----------
 1 file changed, 13 insertions(+), 11 deletions(-)

diff --git a/lib/ExpensiMark.js b/lib/ExpensiMark.js
index 9a2300b7..37656105 100644
--- a/lib/ExpensiMark.js
+++ b/lib/ExpensiMark.js
@@ -470,29 +470,31 @@ export default class ExpensiMark {
             }
             replacedText = replacedText.concat(textToCheck.substr(startIndex, (match.index - startIndex)));
 
-            // We want to avoid matching domains in email addresses so we don't render them as URLs.
-            // If the matched Url has a leading @ sign, The Url's domain can be a domain of email address.
-            // At this case, The matched string should not be parsed as Url,
-            // But it can contain another Url which should be parsed. (e.g. test@expensify.com/https://www.test.com)
-            // So We will retry to apply autolink rule to the string(e.g. /https://www.test.com) except for domain(e.g. expensify.com) after @ sign.
-            let abort = false;
-            let isReparsedByAutolink = false;
+            // 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. test@expensify.com/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(match[2]);
 
+                // 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] !== '')) {
-                    isReparsedByAutolink = true;
                     replacedText = replacedText.concat(domainMatch[1] + this.replace(domainMatch[3], {filterRules: ['autolink']}));
+                    shouldApplyAutoLinkAgain = false;
                 } else {
-                    abort = true;
+                    // Otherwise, we're done applying rules
+                    isDoneMatching = true;
                 }
             }
 
-            if (abort || match[1].includes('
')) {
+            // 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('
')) {
                 replacedText = replacedText.concat(textToCheck.substr(match.index, (match[0].length)));
-            } else if (!isReparsedByAutolink) {
+            } 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)