diff --git a/packages/ckeditor5-link/docs/_snippets/features/autolink.html b/packages/ckeditor5-link/docs/_snippets/features/autolink.html new file mode 100644 index 00000000000..81f60f28594 --- /dev/null +++ b/packages/ckeditor5-link/docs/_snippets/features/autolink.html @@ -0,0 +1,4 @@ + diff --git a/packages/ckeditor5-link/docs/_snippets/features/autolink.js b/packages/ckeditor5-link/docs/_snippets/features/autolink.js new file mode 100644 index 00000000000..627d58cc950 --- /dev/null +++ b/packages/ckeditor5-link/docs/_snippets/features/autolink.js @@ -0,0 +1,23 @@ +/** + * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals console, window, document, ClassicEditor, CS_CONFIG, CKEditorPlugins */ + +ClassicEditor + .create( document.querySelector( '#snippet-autolink' ), { + cloudServices: CS_CONFIG, + extraPlugins: [ + CKEditorPlugins.AutoLink + ], + toolbar: { + viewportTopOffset: window.getViewportTopOffsetConfig() + } + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-link/docs/_snippets/features/build-link-source.js b/packages/ckeditor5-link/docs/_snippets/features/build-link-source.js index 7838022641f..48bfa4a4bfb 100644 --- a/packages/ckeditor5-link/docs/_snippets/features/build-link-source.js +++ b/packages/ckeditor5-link/docs/_snippets/features/build-link-source.js @@ -7,6 +7,11 @@ import ClassicEditor from '@ckeditor/ckeditor5-build-classic/src/ckeditor'; import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config'; +import AutoLink from '@ckeditor/ckeditor5-link/src/autolink'; + +window.CKEditorPlugins = { + AutoLink +}; window.ClassicEditor = ClassicEditor; window.CS_CONFIG = CS_CONFIG; diff --git a/packages/ckeditor5-link/docs/features/link.md b/packages/ckeditor5-link/docs/features/link.md index cff03ce8d6d..fc82692fd1f 100644 --- a/packages/ckeditor5-link/docs/features/link.md +++ b/packages/ckeditor5-link/docs/features/link.md @@ -236,6 +236,27 @@ ClassicEditor .catch( ... ); ``` +## Autolink feature + +You can enable automatic linking of URLs typed or pasted into editor. The `AutoLink` feature will automatically add links to URLs or e-mail addresses. + + + Autolink action can be always reverted using undo (CTRL+Z). + + +{@snippet features/autolink} + +```js +import AutoLink from '@ckeditor/ckeditor5-link/src/autolink'; + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ Link, AutoLink, ... ] + } ) + .then( ... ) + .catch( ... ); +``` + ## Installation diff --git a/packages/ckeditor5-link/package.json b/packages/ckeditor5-link/package.json index 826894826c8..c90147810eb 100644 --- a/packages/ckeditor5-link/package.json +++ b/packages/ckeditor5-link/package.json @@ -21,6 +21,7 @@ "@ckeditor/ckeditor5-basic-styles": "^20.0.0", "@ckeditor/ckeditor5-block-quote": "^20.0.0", "@ckeditor/ckeditor5-clipboard": "^20.0.0", + "@ckeditor/ckeditor5-code-block": "^20.0.0", "@ckeditor/ckeditor5-editor-classic": "^20.0.0", "@ckeditor/ckeditor5-enter": "^20.0.0", "@ckeditor/ckeditor5-paragraph": "^20.0.0", diff --git a/packages/ckeditor5-link/src/autolink.js b/packages/ckeditor5-link/src/autolink.js new file mode 100644 index 00000000000..0ae3df263a8 --- /dev/null +++ b/packages/ckeditor5-link/src/autolink.js @@ -0,0 +1,248 @@ +/** + * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module link/autolink + */ + +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import TextWatcher from '@ckeditor/ckeditor5-typing/src/textwatcher'; +import getLastTextLine from '@ckeditor/ckeditor5-typing/src/utils/getlasttextline'; + +const MIN_LINK_LENGTH_WITH_SPACE_AT_END = 4; // Ie: "t.co " (length 5). + +// This was tweak from https://gist.github.com/dperini/729294. +const URL_REG_EXP = new RegExp( + // Group 1: Line start or after a space. + '(^|\\s)' + + // Group 2: Detected URL (or e-mail). + '(' + + // Protocol identifier or short syntax "//" + // a. Full form http://user@foo.bar.baz:8080/foo/bar.html#baz?foo=bar + '(' + + '(?:(?:(?:https?|ftp):)?\\/\\/)' + + // BasicAuth using user:pass (optional) + '(?:\\S+(?::\\S*)?@)?' + + '(?:' + + // Host & domain names. + '(?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.)+' + + // TLD identifier name. + '(?:[a-z\\u00a1-\\uffff]{2,})' + + ')' + + // port number (optional) + '(?::\\d{2,5})?' + + // resource path (optional) + '(?:[/?#]\\S*)?' + + ')' + + '|' + + // b. Short form (either www.example.com or example@example.com) + '(' + + '(www.|(\\S+@))' + + // Host & domain names. + '((?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.))+' + + // TLD identifier name. + '(?:[a-z\\u00a1-\\uffff]{2,})' + + ')' + + ')$', 'i' ); + +const URL_GROUP_IN_MATCH = 2; + +// Simplified email test - should be run over previously found URL. +const EMAIL_REG_EXP = /^[\S]+@((?![-_])(?:[-\w\u00a1-\uffff]{0,63}[^-_]\.))+(?:[a-z\u00a1-\uffff]{2,})$/i; + +/** + * The auto link plugin. + * + * @extends module:core/plugin~Plugin + */ +export default class AutoLink extends Plugin { + /** + * @inheritDoc + */ + static get pluginName() { + return 'AutoLink'; + } + + /** + * @inheritDoc + */ + init() { + const editor = this.editor; + const selection = editor.model.document.selection; + + selection.on( 'change:range', () => { + // Disable plugin when selection is inside a code block. + this.isEnabled = !selection.anchor.parent.is( 'codeBlock' ); + } ); + + this._enableTypingHandling(); + } + + /** + * @inheritDoc + */ + afterInit() { + this._enableEnterHandling(); + this._enableShiftEnterHandling(); + } + + /** + * Enables auto-link on typing. + * + * @private + */ + _enableTypingHandling() { + const editor = this.editor; + + const watcher = new TextWatcher( editor.model, text => { + // 1. Detect "space" after a text with a potential link. + if ( !isSingleSpaceAtTheEnd( text ) ) { + return; + } + + // 2. Check text before last typed "space". + const url = getUrlAtTextEnd( text.substr( 0, text.length - 1 ) ); + + if ( url ) { + return { url }; + } + } ); + + const input = editor.plugins.get( 'Input' ); + + watcher.on( 'matched:data', ( evt, data ) => { + const { batch, range, url } = data; + + if ( !input.isInput( batch ) ) { + return; + } + + const linkEnd = range.end.getShiftedBy( -1 ); // Executed after a space character. + const linkStart = linkEnd.getShiftedBy( -url.length ); + + const linkRange = editor.model.createRange( linkStart, linkEnd ); + + this._applyAutoLink( url, linkRange ); + } ); + + watcher.bind( 'isEnabled' ).to( this ); + } + + /** + * Enables auto-link on enter key. + * + * @private + */ + _enableEnterHandling() { + const editor = this.editor; + const model = editor.model; + const enterCommand = editor.commands.get( 'enter' ); + + if ( !enterCommand ) { + return; + } + + enterCommand.on( 'execute', () => { + const position = model.document.selection.getFirstPosition(); + + const rangeToCheck = model.createRange( + model.createPositionAt( position.parent.previousSibling, 0 ), + model.createPositionAt( position.parent.previousSibling, 'end' ) + ); + + this._checkAndApplyAutoLinkOnRange( rangeToCheck ); + } ); + } + + /** + * Enables auto-link on shift+enter key. + * + * @private + */ + _enableShiftEnterHandling() { + const editor = this.editor; + const model = editor.model; + + const shiftEnterCommand = editor.commands.get( 'shiftEnter' ); + + if ( !shiftEnterCommand ) { + return; + } + + shiftEnterCommand.on( 'execute', () => { + const position = model.document.selection.getFirstPosition(); + + const rangeToCheck = model.createRange( + model.createPositionAt( position.parent, 0 ), + position.getShiftedBy( -1 ) + ); + + this._checkAndApplyAutoLinkOnRange( rangeToCheck ); + } ); + } + + /** + * Checks passed range if it contains a linkable text. + * + * @param {module:engine/model/range~Range} rangeToCheck + * @private + */ + _checkAndApplyAutoLinkOnRange( rangeToCheck ) { + const model = this.editor.model; + const { text, range } = getLastTextLine( rangeToCheck, model ); + + const url = getUrlAtTextEnd( text ); + + if ( url ) { + const linkRange = model.createRange( + range.end.getShiftedBy( -url.length ), + range.end + ); + + this._applyAutoLink( url, linkRange ); + } + } + + /** + * Applies link on a given range. + * + * @param {String} url URL to link. + * @param {module:engine/model/range~Range} range Text range to apply link attribute. + * @private + */ + _applyAutoLink( url, range ) { + const model = this.editor.model; + + if ( !this.isEnabled || !isLinkAllowedOnRange( range, model ) ) { + return; + } + + // Enqueue change to make undo step. + model.enqueueChange( writer => { + const linkHrefValue = isEmail( url ) ? `mailto://${ url }` : url; + + writer.setAttribute( 'linkHref', linkHrefValue, range ); + } ); + } +} + +// Check if text should be evaluated by the plugin in order to reduce number of RegExp checks on whole text. +function isSingleSpaceAtTheEnd( text ) { + return text.length > MIN_LINK_LENGTH_WITH_SPACE_AT_END && text[ text.length - 1 ] === ' ' && text[ text.length - 2 ] !== ' '; +} + +function getUrlAtTextEnd( text ) { + const match = URL_REG_EXP.exec( text ); + + return match ? match[ URL_GROUP_IN_MATCH ] : null; +} + +function isEmail( linkHref ) { + return EMAIL_REG_EXP.exec( linkHref ); +} + +function isLinkAllowedOnRange( range, model ) { + return model.schema.checkAttributeInSelection( model.createSelection( range ), 'linkHref' ); +} diff --git a/packages/ckeditor5-link/tests/autolink.js b/packages/ckeditor5-link/tests/autolink.js new file mode 100644 index 00000000000..680d8430830 --- /dev/null +++ b/packages/ckeditor5-link/tests/autolink.js @@ -0,0 +1,372 @@ +/** + * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; +import CodeBlockEditing from '@ckeditor/ckeditor5-code-block/src/codeblockediting'; +import Enter from '@ckeditor/ckeditor5-enter/src/enter'; +import Input from '@ckeditor/ckeditor5-typing/src/input'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import ShiftEnter from '@ckeditor/ckeditor5-enter/src/shiftenter'; +import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting'; +import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + +import LinkEditing from '../src/linkediting'; +import AutoLink from '../src/autolink'; + +describe( 'AutoLink', () => { + let editor; + + it( 'should be named', () => { + expect( AutoLink.pluginName ).to.equal( 'AutoLink' ); + } ); + + it( 'should be loaded without Enter & ShiftEnter features', async () => { + const editor = await ModelTestEditor.create( { + plugins: [ Paragraph, Input, LinkEditing, AutoLink ] + } ); + + await editor.destroy(); + } ); + + describe( 'auto link behavior', () => { + let model; + + beforeEach( async () => { + editor = await ModelTestEditor.create( { + plugins: [ Paragraph, Input, Enter, ShiftEnter, LinkEditing, AutoLink ] + } ); + + model = editor.model; + + setData( model, '[]' ); + } ); + + it( 'does nothing on typing normal text', () => { + simulateTyping( 'Cupcake ipsum dolor. Sit amet caramels. Pie jelly-o lemon drops fruitcake.' ); + + expect( getData( model ) ).to.equal( + 'Cupcake ipsum dolor. Sit amet caramels. Pie jelly-o lemon drops fruitcake.[]' + ); + } ); + + it( 'does not add linkHref attribute to a text link while typing', () => { + simulateTyping( 'https://www.cksource.com' ); + + expect( getData( model ) ).to.equal( + 'https://www.cksource.com[]' + ); + } ); + + it( 'adds linkHref attribute to a text link after space', () => { + simulateTyping( 'https://www.cksource.com ' ); + + expect( getData( model ) ).to.equal( + '<$text linkHref="https://www.cksource.com">https://www.cksource.com []' + ); + } ); + + it( 'does not add linkHref attribute if linkHref is not allowed', () => { + model.schema.addAttributeCheck( () => false ); // Disable all attributes. + + simulateTyping( 'https://www.cksource.com ' ); + + expect( getData( model ) ).to.equal( + 'https://www.cksource.com []' + ); + } ); + + it( 'does not add linkHref attribute if plugin is force-disabled (on space)', () => { + editor.plugins.get( 'AutoLink' ).forceDisabled( 'test' ); + + simulateTyping( 'https://www.cksource.com ' ); + + expect( getData( model ) ).to.equal( + 'https://www.cksource.com []' + ); + } ); + + it( 'does not add linkHref attribute if plugin is force-disabled (on enter)', () => { + setData( model, 'https://www.cksource.com[]' ); + editor.plugins.get( 'AutoLink' ).forceDisabled( 'test' ); + + editor.execute( 'enter' ); + + expect( getData( model ) ).to.equal( + 'https://www.cksource.com' + + '[]' + ); + } ); + + it( 'does not add linkHref attribute if plugin is force-disabled (on shift enter)', () => { + setData( model, 'https://www.cksource.com[]' ); + editor.plugins.get( 'AutoLink' ).forceDisabled( 'test' ); + + editor.execute( 'shiftEnter' ); + + expect( getData( model ) ).to.equal( + 'https://www.cksource.com[]' + ); + } ); + + it( 'adds linkHref attribute to a text link after space (inside paragraph)', () => { + setData( model, 'Foo Bar [] Baz' ); + + simulateTyping( 'https://www.cksource.com ' ); + + expect( getData( model ) ).to.equal( + 'Foo Bar <$text linkHref="https://www.cksource.com">https://www.cksource.com [] Baz' + ); + } ); + + it( 'adds linkHref attribute to a text link on shift enter', () => { + setData( model, 'https://www.cksource.com[]' ); + + editor.execute( 'shiftEnter' ); + + // TODO: should test with selection but master has a bug. See: https://github.com/ckeditor/ckeditor5/issues/7459. + expect( getData( model, { withoutSelection: true } ) ).to.equal( + '' + + '<$text linkHref="https://www.cksource.com">https://www.cksource.com' + + '' + + '' + ); + } ); + + it( 'does not add linkHref attribute to a text link after double soft break', () => { + setData( model, 'https://www.cksource.com[]' ); + + editor.execute( 'shiftEnter' ); + + expect( getData( model ) ).to.equal( + 'https://www.cksource.com[]' + ); + } ); + + it( 'adds linkHref attribute to a text link on enter', () => { + setData( model, 'https://www.cksource.com[]' ); + + editor.execute( 'enter' ); + + expect( getData( model ) ).to.equal( + '' + + '<$text linkHref="https://www.cksource.com">https://www.cksource.com' + + '' + + '[]' + ); + } ); + + it( 'adds "mailto://" to link of detected email addresses', () => { + simulateTyping( 'newsletter@cksource.com ' ); + + expect( getData( model ) ).to.equal( + '<$text linkHref="mailto://newsletter@cksource.com">newsletter@cksource.com []' + ); + } ); + + // Some examples came from https://mathiasbynens.be/demo/url-regex. + describe( 'supported URL', () => { + const supportedURLs = [ + 'http://cksource.com', + 'https://cksource.com', + 'https://cksource.com:8080', + 'http://www.cksource.com', + 'hTtP://WwW.cKsOuRcE.cOm', + 'www.cksource.com', + 'http://foo.bar.cksource.com', + 'http://www.cksource.com/some/path/index.html#abc', + 'http://www.cksource.com/some/path/index.html?foo=bar', + 'http://www.cksource.com/some/path/index.html?foo=bar#abc', + 'http://www.cksource.com:8080/some/path/index.html?foo=bar#abc', + 'http://www.cksource.com/some/path/index.html#abc?foo=bar', + 'ftp://cksource.com', + 'http://cksource.com/foo_bar', + 'http://cksource.com/foo_bar/', + 'http://cksource.com/foo_bar_(wikipedia)', + 'http://cksource.com/foo_bar_(wikipedia)_(again)', + 'http://www.cksource.com/wpstyle/?p=364', + 'http://www.cksource.com/wpstyle/?bar=baz&inga=42&quux', + 'http://userid:password@example.com:8080' + + 'http://userid:password@example.com:8080/' + + 'http://userid@cksource.com' + + 'http://userid@cksource.com/' + + 'http://userid@cksource.com:8080' + + 'http://userid@cksource.com:8080/' + + 'http://userid:password@cksource.com' + + 'http://userid:password@cksource.com/' + + 'http://🥳df.ws/123', + 'http://🥳.ws/富', + 'http://🥳.ws', + 'http://🥳.ws/', + 'http://cksource.com/blah_(wikipedia)#cite-1', + 'http://cksource.com/blah_(wikipedia)_blah#cite-1', + 'http://cksource.com/unicode_(🥳)_in_parens', + 'http://cksource.com/(something)?after=parens', + 'http://🥳.cksource.com/', + 'http://code.cksource.com/woot/#&product=browser', + 'http://j.mp', + 'ftp://cksource.com/baz', + 'http://cksource.com/?q=Test%20URL-encoded%20stuff', + 'http://مثال.إختبار', + 'http://例子.测试', + 'http://उदाहरण.परीक्षा', + 'http://1337.net', + 'http://a.b-c.de' + ]; + + for ( const supportedURL of supportedURLs ) { + it( `should detect "${ supportedURL }" as a valid URL`, () => { + simulateTyping( supportedURL + ' ' ); + + expect( getData( model ) ).to.equal( + `<$text linkHref="${ supportedURL }">${ supportedURL } []` ); + } ); + } + } ); + + describe( 'invalid or supported URL', () => { + // Some examples came from https://mathiasbynens.be/demo/url-regex. + const unsupportedOrInvalid = [ + 'http://', + 'http://.', + 'http://..', + 'http://../', + 'http://🥳', + 'http://?', + 'http://??', + 'http://??/', + 'http://#', + 'http://##', + 'http://##/', + '//', + '//a', + '///a', + '///', + 'http:///a', + 'rdar://1234', + 'h://test', + ':// foo bar', + 'ftps://foo.bar/', + 'http://-error-.invalid/', + 'http://localhost', + 'http:/cksource.com', + 'cksource.com', + 'ww.cksource.com' + ]; + + for ( const unsupportedURL of unsupportedOrInvalid ) { + it( `should not detect "${ unsupportedURL }" as a valid URL`, () => { + simulateTyping( unsupportedURL + ' ' ); + + expect( getData( model ) ).to.equal( + `${ unsupportedURL } []` ); + } ); + } + } ); + } ); + + describe( 'Undo integration', () => { + let model; + + beforeEach( async () => { + editor = await ModelTestEditor.create( { + plugins: [ Paragraph, Input, Enter, ShiftEnter, LinkEditing, AutoLink, UndoEditing ] + } ); + + model = editor.model; + + setData( model, 'https://www.cksource.com[]' ); + } ); + + it( 'should undo auto-linking (after space)', () => { + simulateTyping( ' ' ); + + editor.commands.execute( 'undo' ); + + expect( getData( model ) ).to.equal( + 'https://www.cksource.com []' + ); + } ); + + it( 'should undo auto-linking (after )', () => { + editor.execute( 'shiftEnter' ); + + editor.commands.execute( 'undo' ); + + expect( getData( model ) ).to.equal( + 'https://www.cksource.com[]' + ); + } ); + + it( 'should undo auto-linking (after enter)', () => { + editor.execute( 'enter' ); + + editor.commands.execute( 'undo' ); + + expect( getData( model ) ).to.equal( + 'https://www.cksource.com' + + '[]' + ); + } ); + } ); + + describe( 'Code blocks integration', () => { + let model; + + beforeEach( async () => { + editor = await ModelTestEditor.create( { + plugins: [ Paragraph, Input, Enter, ShiftEnter, LinkEditing, AutoLink, CodeBlockEditing ] + } ); + + model = editor.model; + } ); + + it( 'should be disabled inside code blocks (on space)', () => { + setData( model, 'some [] code' ); + + const plugin = editor.plugins.get( 'AutoLink' ); + + simulateTyping( 'www.cksource.com' ); + + expect( plugin.isEnabled ).to.be.false; + expect( getData( model, { withoutSelection: true } ) ) + .to.equal( 'some www.cksource.com code' ); + } ); + + it( 'should be disabled inside code blocks (on enter)', () => { + setData( model, 'some www.cksource.com[] code' ); + + const plugin = editor.plugins.get( 'AutoLink' ); + + editor.execute( 'enter' ); + + expect( plugin.isEnabled ).to.be.false; + expect( getData( model, { withoutSelection: true } ) ).to.equal( + 'some www.cksource.com' + + ' code' + ); + } ); + + it( 'should be disabled inside code blocks (on shift-enter)', () => { + setData( model, 'some www.cksource.com[] code' ); + + const plugin = editor.plugins.get( 'AutoLink' ); + + editor.execute( 'shiftEnter' ); + + expect( plugin.isEnabled ).to.be.false; + expect( getData( model, { withoutSelection: true } ) ).to.equal( + 'some www.cksource.com code' + ); + } ); + } ); + + function simulateTyping( text ) { + const letters = text.split( '' ); + + for ( const letter of letters ) { + editor.execute( 'input', { text: letter } ); + } + } +} ); diff --git a/packages/ckeditor5-link/tests/manual/autolink.html b/packages/ckeditor5-link/tests/manual/autolink.html new file mode 100644 index 00000000000..f1574ee36a0 --- /dev/null +++ b/packages/ckeditor5-link/tests/manual/autolink.html @@ -0,0 +1,11 @@ +
+

Should auto link: http://ckeditor.com

+

+ Danish tootsie roll muffin bonbon muffin candy. Croissant cupcake muffin pastry jujubes sweet roll. Gingerbread jelly donut chocolate muffin ice cream cheesecake pastry. Caramels tiramisu muffin cookie. Tootsie roll liquorice cupcake jelly-o lemon drops lollipop. Cupcake soufflé candy canes danish biscuit tiramisu chocolate chocolate. Sesame snaps caramels brownie. Cookie biscuit biscuit apple pie candy. Chocolate apple pie sweet roll marshmallow wafer jelly beans sweet cake. Bear claw pastry wafer macaroon cake soufflé gummi bears cheesecake sweet. Jelly-o jelly beans halvah apple pie. Powder soufflé donut chocolate. Chocolate cake pie chupa chups donut dessert tootsie roll fruitcake. Apple pie cheesecake bonbon sweet roll tiramisu chupa chups ice cream gummies dessert. + Caramels sweet pie cake carrot cake liquorice. Dessert gingerbread chocolate cake macaroon gummi bears carrot cake sesame snaps. Marshmallow jujubes cake jelly. Tiramisu lollipop chocolate cake. Jelly beans topping gingerbread jelly. Ice cream jujubes liquorice caramels candy canes. Marshmallow fruitcake danish jelly beans macaroon tart chupa chups cake. Dragée cheesecake danish sugar plum marshmallow sweet roll jujubes. Gummi bears marzipan marzipan. Sweet roll jujubes chocolate. Pastry lemon drops dragée sesame snaps ice cream. Donut candy dragée sweet roll. + Candy cupcake carrot cake dragée. Brownie oat cake candy. Fruitcake candy canes cookie muffin sweet roll dessert. Sweet icing halvah dragée muffin. Cotton candy carrot cake croissant sweet caramels halvah jelly beans lemon drops danish. Fruitcake dessert pudding marshmallow sugar plum. Cake cotton candy jelly-o sweet tootsie roll halvah chocolate cake. Lollipop cake marshmallow chocolate chocolate bar. Sesame snaps halvah fruitcake lollipop bonbon bear claw danish chocolate cake. Chupa chups sweet roll candy canes jelly. Danish macaroon ice cream cheesecake cake. Jelly beans caramels fruitcake donut ice cream cookie chupa chups pie. Toffee danish jelly beans chupa chups sweet topping chupa chups lollipop. Oat cake jelly-o pie fruitcake chupa chups. + Gingerbread caramels gummi bears chupa chups topping pie macaroon. Toffee apple pie carrot cake. Cake muffin sesame snaps candy canes cake marzipan carrot cake oat cake. Liquorice tootsie roll chupa chups cake sweet. Fruitcake tootsie roll tart. Caramels lemon drops cookie sweet roll halvah icing carrot cake jelly-o. Chocolate cake jelly muffin candy apple pie tiramisu. Chocolate lollipop gummi bears pie cake marshmallow toffee cheesecake. Gingerbread tootsie roll topping cake pastry. Candy lemon drops bonbon icing fruitcake chupa chups sugar plum. Jelly beans biscuit sugar plum jelly-o cupcake. Macaroon sesame snaps tiramisu. + Cookie jujubes jelly-o candy icing pie bonbon. Chocolate soufflé apple pie jelly beans jujubes. Macaroon pastry danish. Lemon drops lollipop cake bear claw cake. Pastry lemon drops chocolate cake liquorice chocolate toffee. Carrot cake dragée liquorice powder gingerbread bonbon jelly halvah. Lollipop candy canes lollipop candy sugar plum. Danish cake candy. Tiramisu candy jelly. Pudding cookie jelly brownie icing cupcake gingerbread sweet. Powder donut jelly-o sugar plum. Sweet pie gummi bears cake chupa chups bonbon chocolate cake cake danish. Tart cheesecake cheesecake wafer cotton candy. + Should auto link: http://ckeditor.com +

+
diff --git a/packages/ckeditor5-link/tests/manual/autolink.js b/packages/ckeditor5-link/tests/manual/autolink.js new file mode 100644 index 00000000000..277ab6fcf17 --- /dev/null +++ b/packages/ckeditor5-link/tests/manual/autolink.js @@ -0,0 +1,30 @@ +/** + * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals console, window, document */ + +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; + +import Enter from '@ckeditor/ckeditor5-enter/src/enter'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import ShiftEnter from '@ckeditor/ckeditor5-enter/src/shiftenter'; +import Typing from '@ckeditor/ckeditor5-typing/src/typing'; +import Undo from '@ckeditor/ckeditor5-undo/src/undo'; + +import Link from '../../src/link'; +import AutoLink from '../../src/autolink'; +import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ Bold, Typing, Paragraph, Undo, Enter, ShiftEnter, Link, AutoLink ], + toolbar: [ 'link', 'undo', 'redo' ] + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/packages/ckeditor5-link/tests/manual/autolink.md b/packages/ckeditor5-link/tests/manual/autolink.md new file mode 100644 index 00000000000..edded8bf0e6 --- /dev/null +++ b/packages/ckeditor5-link/tests/manual/autolink.md @@ -0,0 +1,25 @@ +## AutoLink feature + +### After a space + +1. Type a URL: + - Staring with `http://`. + - staring with `https://`. + - staring without a protocol (www.cksource.com). + - e-mail address should be linked using `mailto://` (in `linkHref` attribute value only). +2. Type space after a URL. +3. Check if text typed before space get converted to link. + +### After a soft break/new paragraph + +1. Type a URL as in base scenario. +2. Press Enter or Shift+Enter after a link. +3. Check if text typed pressed key get converted to link. + +### Undo integration + +1. Execute auto link either with "space" or with "enter" scenarios. +2. Execute undo. +3. Check if *only* created link was removed: + - For "space" - the space after the text link should be preserved. + - For "enter" - the new block or `` should be preserved.