diff --git a/client/src/components/Draftail/decorators/Link.js b/client/src/components/Draftail/decorators/Link.js index 3a99b8826c..5290be0d7c 100644 --- a/client/src/components/Draftail/decorators/Link.js +++ b/client/src/components/Draftail/decorators/Link.js @@ -144,7 +144,14 @@ const insertContentWithLinks = (editorState, htmlOrText) => { const pattern = new RegExp(linkPatternSource, 'ig'); // Find matches in the block, confirm the URL, create the entity, store the range. const matches = Array.from(blockText.matchAll(pattern), (match) => { - const url = getValidLinkURL(match[1]); + // Account for punctuation chars valid in URLs but unlikely to be intended. + // For example "Go to https://example.com." + // Terminal Punctuation class: see https://www.unicode.org/review/pr-23.html. + const cleanURLPattern = match[1].replace( + /\p{Terminal_Punctuation}$/u, + '', + ); + const url = getValidLinkURL(cleanURLPattern); if (!url) return {}; @@ -152,7 +159,7 @@ const insertContentWithLinks = (editorState, htmlOrText) => { return { start: match.index, - end: match.index + match[1].length, + end: match.index + cleanURLPattern.length, key: content.getLastCreatedEntityKey(), }; }); diff --git a/client/src/components/Draftail/decorators/Link.test.js b/client/src/components/Draftail/decorators/Link.test.js index 5b2a492d35..4ac0d194f8 100644 --- a/client/src/components/Draftail/decorators/Link.test.js +++ b/client/src/components/Draftail/decorators/Link.test.js @@ -276,4 +276,45 @@ describe('onPasteLink', () => { 1: { data: { url: 'http://test.co/' } }, }); }); + + it('skips linking punctuation chars', () => { + const punctuation = { + // Characters that will be removed. + '.': '', + '?': '', + '!': '', + ':': '', + ';': '', + ',': '', + // Syriac Harklean Metobelus + '܌': '', + '؟': '', + '،': '', + '‼': '', + '﹒': '', + // Characters that will be preserved. + '…': '…', + '-': '-', + '_': '_', + '–': '–', + '+': '+', + '=': '=', + }; + const input = Object.keys(punctuation) + .map((punc) => `

http://a.co/t${punc}/${punc}

`) + .join(' '); + const raw = testOnPasteOutput(input.replace(/<[^>]+/g), input); + + expect(raw.blocks.map(({ text }) => text)).toMatchSnapshot(); + expect( + Object.values(raw.entityMap).map((entity) => entity.data.url), + ).toMatchSnapshot(); + + const expectedLength = Object.keys(punctuation).map( + (punc) => `http://a.co/t${punc}/`.length + punctuation[punc].length, + ); + expect( + raw.blocks.map(({ entityRanges }) => entityRanges[0].length), + ).toEqual(expectedLength); + }); }); diff --git a/client/src/components/Draftail/decorators/__snapshots__/Link.test.js.snap b/client/src/components/Draftail/decorators/__snapshots__/Link.test.js.snap index 3f71e9df00..b3759bbc23 100644 --- a/client/src/components/Draftail/decorators/__snapshots__/Link.test.js.snap +++ b/client/src/components/Draftail/decorators/__snapshots__/Link.test.js.snap @@ -67,3 +67,47 @@ exports[`Link works 1`] = ` test `; + +exports[`onPasteLink skips linking punctuation chars 1`] = ` +[ + "http://a.co/t./.", + "http://a.co/t?/?", + "http://a.co/t!/!", + "http://a.co/t:/:", + "http://a.co/t;/;", + "http://a.co/t,/,", + "http://a.co/t܌/܌", + "http://a.co/t؟/؟", + "http://a.co/t،/،", + "http://a.co/t‼/‼", + "http://a.co/t﹒/﹒", + "http://a.co/t…/…", + "http://a.co/t-/-", + "http://a.co/t_/_", + "http://a.co/t–/–", + "http://a.co/t+/+", + "http://a.co/t=/=hello", +] +`; + +exports[`onPasteLink skips linking punctuation chars 2`] = ` +[ + "http://a.co/t./", + "http://a.co/t?/", + "http://a.co/t!/", + "http://a.co/t:/", + "http://a.co/t;/", + "http://a.co/t,/", + "http://a.co/t܌/", + "http://a.co/t؟/", + "http://a.co/t،/", + "http://a.co/t‼/", + "http://a.co/t﹒/", + "http://a.co/t…/…", + "http://a.co/t-/-", + "http://a.co/t_/_", + "http://a.co/t–/–", + "http://a.co/t+/+", + "http://a.co/t=/=", +] +`;