diff --git a/__tests__/ExpensiMark-HTML-test.js b/__tests__/ExpensiMark-HTML-test.js index 476655e2..b6935396 100644 --- a/__tests__/ExpensiMark-HTML-test.js +++ b/__tests__/ExpensiMark-HTML-test.js @@ -159,8 +159,12 @@ test('Test markdown replacement for emails wrapped in bold/strikethrough/italic // Markdown style links replaced successfully test('Test markdown style links', () => { - const testString = 'Go to [Expensify](https://www.expensify.com) to learn more. [Expensify](www.expensify.com) [Expensify](expensify.com) [It\'s really the coolest](expensify.com) [`Some` Special cases - + . = , \'](expensify.com/some?query=par|am)'; - const resultString = 'Go to Expensify to learn more. Expensify Expensify It's really the coolest Some Special cases - + . = , ''; + let testString = 'Go to [Expensify](https://www.expensify.com) to learn more. [Expensify](www.expensify.com) [Expensify](expensify.com) [It\'s really the coolest](expensify.com) [`Some` Special cases - + . = , \'](expensify.com/some?query=par|am)'; + let resultString = 'Go to Expensify to learn more. Expensify Expensify It's really the coolest Some Special cases - + . = , ''; + expect(parser.replace(testString)).toBe(resultString); + + testString = 'Go to [Localhost](http://localhost:3000) to learn more. [Expensify](www.expensify.com) [Expensify](expensify.com) [It\'s really the coolest](expensify.com) [`Some` Special cases - + . = , \'](expensify.com/some?query=par|am)'; + resultString = 'Go to Localhost to learn more. Expensify Expensify It's really the coolest Some Special cases - + . = , ''; expect(parser.replace(testString)).toBe(resultString); }); @@ -181,7 +185,8 @@ test('Test critical markdown style links', () => { + '[link with an @ in it](https://google.com) ' + '[link with [brackets] inside of it](https://google.com) ' + '[link with smart quotes ‘’“”](https://google.com) ' - + '[link with someone@expensify.com email in it](https://google.com)'; + + '[link with someone@expensify.com email in it](https://google.com)' + + '[Localhost](http://a:3030)'; const resultString = 'Testing ' + 'strikethrough bold italic ' + 'strikethrough bold italic test.com ' @@ -197,7 +202,8 @@ test('Test critical markdown style links', () => { + 'link with an @ in it ' + 'link with [brackets] inside of it ' + 'link with smart quotes ‘’“” ' - + 'link with someone@expensify.com email in it'; + + 'link with someone@expensify.com email in it' + + 'Localhost'; expect(parser.replace(testString)).toBe(resultString); }); diff --git a/__tests__/ExpensiMark-Markdown-test.js b/__tests__/ExpensiMark-Markdown-test.js index fa7dbbd1..857086e7 100644 --- a/__tests__/ExpensiMark-Markdown-test.js +++ b/__tests__/ExpensiMark-Markdown-test.js @@ -590,8 +590,11 @@ test('Test html to heading1 markdown when there are adjacent h1 tags in the line }); test('Test link with multiline text do not loses markdown', () => { - const testString = 'multiline\ntext'; - const resultString = '[multiline\ntext](https://google.com/)'; + let testString = 'multiline\ntext'; + let resultString = '[multiline\ntext](https://google.com/)'; + expect(parser.htmlToMarkdown(testString)).toBe(resultString); + testString = 'multiline\ntext'; + resultString = '[multiline\ntext](http://localhost:3000/)'; expect(parser.htmlToMarkdown(testString)).toBe(resultString); }); @@ -614,8 +617,12 @@ test('Map html list item to newline with two prefix spaces', () => { }); test('Test list item replacement when there is an anchor tag inside
  • tag', () => { - const testString = ''; - const resultString = ' [Coffee](https://example.com) -- Coffee\n [Tea](https://example.com) -- Tea\n [Milk](https://example.com) -- Milk'; + let testString = ''; + let resultString = ' [Coffee](https://example.com) -- Coffee\n [Tea](https://example.com) -- Tea\n [Milk](https://example.com) -- Milk'; + expect(parser.htmlToMarkdown(testString)).toBe(resultString); + + testString = ''; + resultString = ' [Coffee](http://localhost) -- Coffee\n [Tea](http://localhost/) -- Tea\n [Milk](http://localhost/) -- Milk'; expect(parser.htmlToMarkdown(testString)).toBe(resultString); }); diff --git a/__tests__/URL-test.js b/__tests__/URL-test.js index 820f91f5..553f4b55 100644 --- a/__tests__/URL-test.js +++ b/__tests__/URL-test.js @@ -1,39 +1,59 @@ -import {URL_REGEX_WITH_REQUIRED_PROTOCOL, URL_REGEX} from '../lib/Url'; +import {URL_REGEX_WITH_REQUIRED_PROTOCOL, URL_REGEX, LOOSE_URL_REGEX} from '../lib/Url'; -describe('Mandatory protocol for URL', () => { - it('correctly tests valid urls', () => { - const regexToTest = new RegExp(`^${URL_REGEX_WITH_REQUIRED_PROTOCOL}$`, 'i'); - expect(regexToTest.test('https://google.com/')).toBeTruthy(); - expect(regexToTest.test('http://google.com/')).toBeTruthy(); - expect(regexToTest.test('ftp://google.com/')).toBeTruthy(); - expect(regexToTest.test('https://we.are.expensify.com/how-we-got-here')).toBeTruthy(); - expect(regexToTest.test('https://google.com:12')).toBeTruthy(); - expect(regexToTest.test('https://google.com:65535')).toBeTruthy(); - expect(regexToTest.test('https://google.com:65535/path/my')).toBeTruthy(); +describe('Strict URL validation', () => { + describe('Mandatory protocol for URL', () => { + it('correctly tests valid urls', () => { + const regexToTest = new RegExp(`^${URL_REGEX_WITH_REQUIRED_PROTOCOL}$`, 'i'); + expect(regexToTest.test('https://google.com/')).toBeTruthy(); + expect(regexToTest.test('http://google.com/')).toBeTruthy(); + expect(regexToTest.test('ftp://google.com/')).toBeTruthy(); + expect(regexToTest.test('https://we.are.expensify.com/how-we-got-here')).toBeTruthy(); + expect(regexToTest.test('https://google.com:12')).toBeTruthy(); + expect(regexToTest.test('https://google.com:65535')).toBeTruthy(); + expect(regexToTest.test('https://google.com:65535/path/my')).toBeTruthy(); + }); + it('correctly tests invalid urls', () => { + const regexToTest = new RegExp(`^${URL_REGEX_WITH_REQUIRED_PROTOCOL}$`, 'i'); + expect(regexToTest.test('google.com')).toBeFalsy(); + expect(regexToTest.test('https://google.com:02')).toBeFalsy(); + expect(regexToTest.test('https://google.com:65536')).toBeFalsy(); + expect(regexToTest.test('smtp://google.com')).toBeFalsy(); + }); }); - it('correctly tests invalid urls', () => { - const regexToTest = new RegExp(`^${URL_REGEX_WITH_REQUIRED_PROTOCOL}$`, 'i'); - expect(regexToTest.test('google.com')).toBeFalsy(); - expect(regexToTest.test('https://google.com:02')).toBeFalsy(); - expect(regexToTest.test('https://google.com:65536')).toBeFalsy(); - expect(regexToTest.test('smtp://google.com')).toBeFalsy(); + + describe('Optional protocol for URL', () => { + it('correctly tests valid urls', () => { + const regexToTest = new RegExp(`^${URL_REGEX}$`, 'i'); + expect(regexToTest.test('google.com/')).toBeTruthy(); + expect(regexToTest.test('https://google.com/')).toBeTruthy(); + expect(regexToTest.test('ftp://google.com/')).toBeTruthy(); + expect(regexToTest.test('we.are.expensify.com/how-we-got-here')).toBeTruthy(); + expect(regexToTest.test('google.com:12')).toBeTruthy(); + expect(regexToTest.test('google.com:65535')).toBeTruthy(); + expect(regexToTest.test('google.com:65535/path/my')).toBeTruthy(); + }); + it('correctly tests invalid urls', () => { + const regexToTest = new RegExp(`^${URL_REGEX}$`, 'i'); + expect(regexToTest.test('google.com:02')).toBeFalsy(); + expect(regexToTest.test('google.com:65536')).toBeFalsy(); + }); }); }); -describe('Optional protocol for URL', () => { - it('correctly tests valid urls', () => { - const regexToTest = new RegExp(`^${URL_REGEX}$`, 'i'); - expect(regexToTest.test('google.com/')).toBeTruthy(); - expect(regexToTest.test('https://google.com/')).toBeTruthy(); - expect(regexToTest.test('ftp://google.com/')).toBeTruthy(); - expect(regexToTest.test('we.are.expensify.com/how-we-got-here')).toBeTruthy(); - expect(regexToTest.test('google.com:12')).toBeTruthy(); - expect(regexToTest.test('google.com:65535')).toBeTruthy(); - expect(regexToTest.test('google.com:65535/path/my')).toBeTruthy(); +describe('Loose URL validation', () => { + it('correctly tests urls that can be valid on local machine', () => { + const regexToTest = new RegExp(`^${LOOSE_URL_REGEX}$`, 'i'); + expect(regexToTest.test('http://localhost:3000')).toBeTruthy(); + expect(regexToTest.test('https://local.url')).toBeTruthy(); + expect(regexToTest.test('http://a.b')).toBeTruthy(); + expect(regexToTest.test('http://expensify')).toBeTruthy(); + expect(regexToTest.test('http://google.com/abcd')).toBeTruthy(); }); + it('correctly tests invalid urls', () => { - const regexToTest = new RegExp(`^${URL_REGEX}$`, 'i'); - expect(regexToTest.test('google.com:02')).toBeFalsy(); - expect(regexToTest.test('google.com:65536')).toBeFalsy(); + const regexToTest = new RegExp(`^${LOOSE_URL_REGEX}$`, 'i'); + expect(regexToTest.test('localhost:3000')).toBeFalsy(); + expect(regexToTest.test('local.url')).toBeFalsy(); + expect(regexToTest.test('https://otherexample.com links get rendered first')).toBeFalsy(); }); }); diff --git a/lib/ExpensiMark.js b/lib/ExpensiMark.js index 84230c0e..1a24840e 100644 --- a/lib/ExpensiMark.js +++ b/lib/ExpensiMark.js @@ -1,6 +1,6 @@ import _ from 'underscore'; import Str from './str'; -import {URL_REGEX} from './Url'; +import {MARKDOWN_URL_REGEX, LOOSE_URL_REGEX, URL_REGEX} from './Url'; import {CONST} from './CONST'; const SLACK_SPAN_NEW_LINE_TAG = ''; @@ -107,7 +107,7 @@ export default class ExpensiMark { name: 'link', process: (textToProcess, replacement) => { const regex = new RegExp( - `\\[([^\\][]*(?:\\[[^\\][]*][^\\][]*)*)]\\(${URL_REGEX}\\)(?![^<]*(<\\/pre>|<\\/code>))`, + `\\[([^\\][]*(?:\\[[^\\][]*][^\\][]*)*)]\\(${MARKDOWN_URL_REGEX}\\)(?![^<]*(<\\/pre>|<\\/code>))`, 'gi', ); return this.modifyTextForUrlLinks(regex, textToProcess, replacement); @@ -144,7 +144,7 @@ export default class ExpensiMark { process: (textToProcess, replacement) => { const regex = new RegExp( - `(?![^<]*>|[^<>]*<\\/)([_*~]*?)${URL_REGEX}\\1(?!((?:(?!|[^<]*(<\\/pre>|<\\/code>))`, + `(?![^<]*>|[^<>]*<\\/)([_*~]*?)${MARKDOWN_URL_REGEX}\\1(?!((?:(?!|[^<]*(<\\/pre>|<\\/code>))`, 'gi', ); return this.modifyTextForUrlLinks(regex, textToProcess, replacement); @@ -448,7 +448,7 @@ export default class ExpensiMark { if (abort) { replacedText = replacedText.concat(textToCheck.substr(match.index, (match[0].length))); } else { - const urlRegex = new RegExp(`^${URL_REGEX}$`, 'i'); + 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) // At the entry of function this.replace, text is already escaped due to the rules that precede the link diff --git a/lib/Url.js b/lib/Url.js index 495c1f38..f8eb7e9e 100644 --- a/lib/Url.js +++ b/lib/Url.js @@ -11,6 +11,12 @@ const URL_REGEX = `((${URL_WEBSITE_REGEX})${URL_PATH_REGEX}(?:${URL_PARAM_REGEX} const URL_REGEX_WITH_REQUIRED_PROTOCOL = URL_REGEX.replace(`${URL_PROTOCOL_REGEX}?`, URL_PROTOCOL_REGEX); +const LOOSE_URL_WEBSITE_REGEX = `${URL_PROTOCOL_REGEX}([-\\w]+(\\.[-\\w]+)*)(?:\\:${ALLOWED_PORTS}|\\b|(?=_))`; +const LOOSE_URL_REGEX = `((${LOOSE_URL_WEBSITE_REGEX})${URL_PATH_REGEX}(?:${URL_PARAM_REGEX}|${URL_FRAGMENT_REGEX})*)`; + + +const MARKDOWN_URL_REGEX = `(${LOOSE_URL_REGEX}|${URL_REGEX})`; + export { URL_WEBSITE_REGEX, URL_PATH_REGEX, @@ -19,4 +25,7 @@ export { URL_REGEX, URL_REGEX_WITH_REQUIRED_PROTOCOL, URL_PROTOCOL_REGEX, + LOOSE_URL_REGEX, + LOOSE_URL_WEBSITE_REGEX, + MARKDOWN_URL_REGEX, };