diff --git a/CHANGES.md b/CHANGES.md index 7f508f0b8c2..bfdf6b483e7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,10 @@ ## CKEditor 4.10 +New Features: + +* [#1761](https://github.com/ckeditor/ckeditor-dev/issues/1761): [Autolink](https://ckeditor.com/cke4/addon/autolink) plugin supports email links. + Fixed Issues: * [#1458](https://github.com/ckeditor/ckeditor-dev/issues/1458): [Edge] Fixed: After blurring editor it takes 2 clicks to focus a widget. diff --git a/plugins/autolink/plugin.js b/plugins/autolink/plugin.js index 732dc690fb6..5e19551eba0 100644 --- a/plugins/autolink/plugin.js +++ b/plugins/autolink/plugin.js @@ -8,6 +8,8 @@ // Regex by Imme Emosol. var validUrlRegex = /^(https?|ftp):\/\/(-\.)?([^\s\/?\.#]+\.?)+(\/[^\s]*)?[^\s\.,]$/ig, + // Regex by (https://www.w3.org/TR/html5/forms.html#valid-e-mail-address). + validEmailRegex = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/g, doubleQuoteRegex = /"/g; CKEDITOR.plugins.add( 'autolink', { @@ -26,8 +28,14 @@ return; } - // https://dev.ckeditor.com/ticket/13419 - data = data.replace( validUrlRegex , '$&' ); + // Create valid email links (#1761). + if ( data.match( validEmailRegex ) ) { + data = data.replace( validEmailRegex, '$&' ); + data = tryToEncodeLink( data ); + } else { + // https://dev.ckeditor.com/ticket/13419 + data = data.replace( validUrlRegex , '$&' ); + } // If link was discovered, change the type to 'html'. This is important e.g. when pasting plain text in Chrome // where real type is correctly recognized. @@ -37,6 +45,25 @@ evt.data.dataValue = data; } ); + + function tryToEncodeLink( data ) { + // If enabled use link plugin to encode email link. + if ( editor.plugins.link ) { + var link = CKEDITOR.dom.element.createFromHtml( data ), + linkData = CKEDITOR.plugins.link.parseLinkAttributes( editor, link ), + attributes = CKEDITOR.plugins.link.getLinkAttributes( editor, linkData ); + + if ( !CKEDITOR.tools.isEmpty( attributes.set ) ) { + link.setAttributes( attributes.set ); + } + + if ( attributes.removed.length ) { + link.removeAttributes( attributes.removed ); + } + return link.getOuterHtml(); + } + return data; + } } } ); } )(); diff --git a/tests/plugins/autolink/autolink.js b/tests/plugins/autolink/autolink.js index 8242d63817f..f3f5ea85bf3 100644 --- a/tests/plugins/autolink/autolink.js +++ b/tests/plugins/autolink/autolink.js @@ -5,28 +5,34 @@ /* bender-include: ../clipboard/_helpers/pasting.js */ /* global assertPasteEvent */ -bender.editor = { - config: { - allowedContent: true, - pasteFilter: null +bender.editors = { + classic: { + config: { + allowedContent: true, + pasteFilter: null + } + }, + encodedDefault: { + config: { + allowedContent: true, + pasteFilter: null, + emailProtection: 'encode', + extraPlugins: 'link' + } + }, + encodedCustom: { + config: { + allowedContent: true, + pasteFilter: null, + emailProtection: 'mt(NAME,DOMAIN,SUBJECT,BODY)', + extraPlugins: 'link' + } } }; bender.test( { - 'test normal link': function() { - var pastedText = 'https://placekitten.com/g/180/300', - expected = '' + pastedText + ''; - - assertPasteEvent( this.editor, { dataValue: pastedText }, { dataValue: expected, type: 'html' } ); - }, - 'test fake link': function() { - var pastedText = 'https//placekitten.com/g/190/300'; - - assertPasteEvent( this.editor, { dataValue: pastedText }, { dataValue: pastedText, type: 'html' } ); - }, - - 'test link with HTML tags': function() { + 'test URL link with HTML tags': function() { var pastedTexts = [ 'https://
placekitten.com/g/200/301', 'https://
placekitten.com/g/200/302', @@ -37,43 +43,76 @@ bender.test( { var pastedText; while ( ( pastedText = pastedTexts.pop() ) ) { - this.editor.once( 'paste', function( evt ) { + this.editors.classic.once( 'paste', function( evt ) { + evt.cancel(); + + assert.areSame( -1, evt.data.dataValue.search( /mail@example.com', + '
mail@example.com', + '
mail@example.com', + 'mail@example.com' + ]; + + var pastedText; + + while ( ( pastedText = pastedTexts.pop() ) ) { + this.editors.classic.once( 'paste', function( evt ) { evt.cancel(); assert.areSame( -1, evt.data.dataValue.search( /
' + pastedText + '', evt.data.dataValue ); }, null, null, 900 ); - this.editor.execCommand( 'paste', pastedText ); + this.editors.classic.execCommand( 'paste', pastedText ); + } + }, + + 'test various valid email links': function() { + var pastedTexts = [ + 'mail@example.com', + 'mail@mail', + ".!#$%&'*+-/=?^_`{|}~@1234567890", + 'mail@192.168.20.99' + ]; + + var pastedText; + + while ( ( pastedText = pastedTexts.pop() ) ) { + this.editors.classic.once( 'paste', function( evt ) { + evt.cancel(); + + assert.areSame( '' + pastedText + '', evt.data.dataValue ); + }, null, null, 900 ); + + this.editors.classic.execCommand( 'paste', pastedText ); } }, - 'test various invalid links': function() { + 'test various invalid URL links': function() { var pastedTexts = [ + 'https//placekitten.com/g/190/300', 'https://placekitten.com/g/181/300.', 'http://giphy.com?search', 'https://www.google.pl,,,,', @@ -106,64 +167,115 @@ bender.test( { var pastedText; while ( ( pastedText = pastedTexts.pop() ) ) { - this.editor.once( 'paste', function( evt ) { + this.editors.classic.once( 'paste', function( evt ) { + evt.cancel(); + + assert.areSame( pastedText, evt.data.dataValue ); + }, null, null, 900 ); + + this.editors.classic.execCommand( 'paste', pastedText ); + } + }, + + 'test various invalid email links': function() { + var pastedTexts = [ + 'mail@', + '@mail', + 'hello,world@host.com', + 'example@hello,world', + '"@emai.com', + '"example"@email.com' + ]; + + var pastedText; + + while ( ( pastedText = pastedTexts.pop() ) ) { + this.editors.classic.once( 'paste', function( evt ) { evt.cancel(); assert.areSame( pastedText, evt.data.dataValue ); }, null, null, 900 ); - this.editor.execCommand( 'paste', pastedText ); + this.editors.classic.execCommand( 'paste', pastedText ); } }, - 'test pasting multiple links': function() { + 'test pasting multiple URL links': function() { var pastedText = 'http://en.wikipedia.org/wiki/Weasel http://en.wikipedia.org/wiki/Weasel'; - assertPasteEvent( this.editor, { dataValue: pastedText }, { dataValue: pastedText, type: 'html' } ); + assertPasteEvent( this.editors.classic, { dataValue: pastedText }, { dataValue: pastedText, type: 'html' } ); + }, + + 'test pasting multiple email links': function() { + var pastedText = 'example1@mail.com example2@mail.com'; + + assertPasteEvent( this.editors.classic, { dataValue: pastedText }, { dataValue: pastedText, type: 'html' } ); }, 'test pasting whole paragraph': function() { var pastedText = 'A multi-channel operating strategy' + 'drives the enabler, while the enablers strategically embrace the game-changing, ' + - 'organic and cross-enterprise cultures.'; + 'organic and cross-enterprise ' + + 'cultures.'; - assertPasteEvent( this.editor, { dataValue: pastedText }, { dataValue: pastedText, type: 'html' } ); + assertPasteEvent( this.editors.classic, { dataValue: pastedText }, { dataValue: pastedText, type: 'html' } ); }, 'test content that is a link': function() { var pastedText = 'Weasel'; - assertPasteEvent( this.editor, { dataValue: pastedText }, { dataValue: pastedText, type: 'html' } ); + assertPasteEvent( this.editors.classic, { dataValue: pastedText }, { dataValue: pastedText, type: 'html' } ); }, 'test type is changed once a link is created': function() { var pastedText = 'https://placekitten.com/g/180/300', expected = '' + pastedText + ''; - assertPasteEvent( this.editor, { dataValue: pastedText, type: 'text' }, { dataValue: expected, type: 'html' } ); + assertPasteEvent( this.editors.classic, { dataValue: pastedText, type: 'text' }, { dataValue: expected, type: 'html' } ); }, 'test type is not changed if link was not found': function() { var pastedText = 'foo bar'; - assertPasteEvent( this.editor, { dataValue: pastedText, type: 'text' }, { dataValue: pastedText, type: 'text' } ); + assertPasteEvent( this.editors.classic, { dataValue: pastedText, type: 'text' }, { dataValue: pastedText, type: 'text' } ); }, 'test internal paste is not autolinked': function() { - var editor = this.editor, + var editor = this.editors.classic, pastedText = 'https://foo.bar/g/185/310'; - this.editor.once( 'paste', function( evt ) { + this.editors.classic.once( 'paste', function( evt ) { evt.data.dataTransfer.sourceEditor = editor; }, null, null, 1 ); - this.editor.once( 'paste', function( evt ) { + this.editors.classic.once( 'paste', function( evt ) { evt.cancel(); assert.areSame( pastedText, evt.data.dataValue ); }, null, null, 900 ); - this.editor.execCommand( 'paste', pastedText ); + this.editors.classic.execCommand( 'paste', pastedText ); + }, + + 'test created protected mail link (function)': function() { + var pastedText = 'a@a', + expected = 'a@a'; + + assertPasteEvent( this.editors.encodedDefault, { dataValue: pastedText, type: 'text' }, function( data ) { + assert.areEqual( 'html', data.type ); + assert.areEqual( expected, bender.tools.compatHtml( data.dataValue ) ); + } ); + }, + + 'test created protected mail link (string)': function() { + var pastedText = 'a@a', + expected = 'a@a'; + + assertPasteEvent( this.editors.encodedCustom, { dataValue: pastedText, type: 'text' }, function( data ) { + assert.areEqual( 'html', data.type ); + assert.areEqual( expected, bender.tools.compatHtml( data.dataValue ) ); + } ); } } ); diff --git a/tests/plugins/autolink/manual/mail.html b/tests/plugins/autolink/manual/mail.html new file mode 100644 index 00000000000..b172c879374 --- /dev/null +++ b/tests/plugins/autolink/manual/mail.html @@ -0,0 +1,25 @@ +

Emails that should be turned into links

+ + + + + +

Emails that shouldn't be turned into links

+ + + + + + diff --git a/tests/plugins/autolink/manual/mail.md b/tests/plugins/autolink/manual/mail.md new file mode 100644 index 00000000000..bc1d6b836f6 --- /dev/null +++ b/tests/plugins/autolink/manual/mail.md @@ -0,0 +1,10 @@ +@bender-ui: collapsed +@bender-tags: 4.10.0, feature, 1761 +@bender-ckeditor-plugins: toolbar, wysiwygarea, link, autolink + +## Test Scenario + +Paste each email to the editor and check whether it's turned into a link. + + +Make sure you copy the emails without trailing spaces.