From 26c1cee6e6f0fe27444b442dfdf9dc056fb6d802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 17 Jun 2020 11:13:18 +0200 Subject: [PATCH 01/30] Create AutoLink plugin class. --- packages/ckeditor5-link/src/autolink.js | 24 +++++++++++++++++++++++ packages/ckeditor5-link/tests/autolink.js | 12 ++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 packages/ckeditor5-link/src/autolink.js create mode 100644 packages/ckeditor5-link/tests/autolink.js diff --git a/packages/ckeditor5-link/src/autolink.js b/packages/ckeditor5-link/src/autolink.js new file mode 100644 index 00000000000..79070687330 --- /dev/null +++ b/packages/ckeditor5-link/src/autolink.js @@ -0,0 +1,24 @@ +/** + * @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'; + +/** + * The auto link plugin. + * + * @extends module:core/plugin~Plugin + */ +export default class AutoLink extends Plugin { + /** + * @inheritDoc + */ + static get pluginName() { + return 'AutoLink'; + } +} diff --git a/packages/ckeditor5-link/tests/autolink.js b/packages/ckeditor5-link/tests/autolink.js new file mode 100644 index 00000000000..2d07a8eae72 --- /dev/null +++ b/packages/ckeditor5-link/tests/autolink.js @@ -0,0 +1,12 @@ +/** + * @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 AutoLink from '../src/autolink'; + +describe( 'AutoLink', () => { + it( 'should be named', () => { + expect( AutoLink.pluginName ).to.equal( 'AutoLink' ); + } ); +} ); From bbdc501af2706af3280c08ac11471b0382cbe578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 17 Jun 2020 11:25:42 +0200 Subject: [PATCH 02/30] Add tests for auto link expected behavior. --- packages/ckeditor5-link/tests/autolink.js | 49 +++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/packages/ckeditor5-link/tests/autolink.js b/packages/ckeditor5-link/tests/autolink.js index 2d07a8eae72..88a83461e4b 100644 --- a/packages/ckeditor5-link/tests/autolink.js +++ b/packages/ckeditor5-link/tests/autolink.js @@ -3,10 +3,59 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ +import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + +import Link from '../src/link'; import AutoLink from '../src/autolink'; describe( 'AutoLink', () => { it( 'should be named', () => { expect( AutoLink.pluginName ).to.equal( 'AutoLink' ); } ); + + describe( 'auto link behavior', () => { + let editor; + + beforeEach( async () => { + editor = ModelTestEditor.create( { plugins: [ Paragraph, Link, AutoLink ] } ); + + setData( editor.model, '[]' ); + } ); + + it( 'does not add linkHref attribute to a text link while typing', () => { + simulateTyping( editor, 'https://www.cksource.com' ); + + expect( getData( editor.model ) ).to.equal( + 'https://www.cksource.com[]' + ); + } ); + + it( 'adds linkHref attribute to a text link after space', () => { + simulateTyping( editor, 'https://www.cksource.com ' ); + + expect( getData( editor.model ) ).to.equal( + '<$text linkHref="https://www.cksource.com">https://www.cksource.com []' + ); + } ); + + it( 'can undo auto-linking', () => { + simulateTyping( editor, 'https://www.cksource.com ' ); + + editor.commands.execute( 'undo' ); + + expect( getData( editor.model ) ).to.equal( + 'https://www.cksource.com []' + ); + } ); + + function simulateTyping( text ) { + const letters = text.split( '' ); + + for ( const letter of letters ) { + editor.execute( 'input', { text: letter } ); + } + } + } ); } ); From 9bde7e83c27acbd9b4f7f4be9730b04fa286c284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 17 Jun 2020 11:58:58 +0200 Subject: [PATCH 03/30] Fix autolink tests. --- packages/ckeditor5-link/tests/autolink.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/ckeditor5-link/tests/autolink.js b/packages/ckeditor5-link/tests/autolink.js index 88a83461e4b..a9851aadb0d 100644 --- a/packages/ckeditor5-link/tests/autolink.js +++ b/packages/ckeditor5-link/tests/autolink.js @@ -4,10 +4,11 @@ */ import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; +import Input from '@ckeditor/ckeditor5-typing/src/input'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; -import Link from '../src/link'; +import LinkEditing from '../src/linkediting'; import AutoLink from '../src/autolink'; describe( 'AutoLink', () => { @@ -19,13 +20,13 @@ describe( 'AutoLink', () => { let editor; beforeEach( async () => { - editor = ModelTestEditor.create( { plugins: [ Paragraph, Link, AutoLink ] } ); + editor = await ModelTestEditor.create( { plugins: [ Paragraph, Input, LinkEditing, AutoLink ] } ); setData( editor.model, '[]' ); } ); it( 'does not add linkHref attribute to a text link while typing', () => { - simulateTyping( editor, 'https://www.cksource.com' ); + simulateTyping( 'https://www.cksource.com' ); expect( getData( editor.model ) ).to.equal( 'https://www.cksource.com[]' @@ -33,7 +34,7 @@ describe( 'AutoLink', () => { } ); it( 'adds linkHref attribute to a text link after space', () => { - simulateTyping( editor, 'https://www.cksource.com ' ); + simulateTyping( 'https://www.cksource.com ' ); expect( getData( editor.model ) ).to.equal( '<$text linkHref="https://www.cksource.com">https://www.cksource.com []' @@ -41,7 +42,7 @@ describe( 'AutoLink', () => { } ); it( 'can undo auto-linking', () => { - simulateTyping( editor, 'https://www.cksource.com ' ); + simulateTyping( 'https://www.cksource.com ' ); editor.commands.execute( 'undo' ); From 026ec376caa88e9614638fb514f35e8f6d93d483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 18 Jun 2020 14:03:52 +0200 Subject: [PATCH 04/30] Initial AutoLink implementation using text watcher. --- packages/ckeditor5-link/src/autolink.js | 38 +++++++++++++++++++++++ packages/ckeditor5-link/tests/autolink.js | 3 +- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-link/src/autolink.js b/packages/ckeditor5-link/src/autolink.js index 79070687330..e4415360829 100644 --- a/packages/ckeditor5-link/src/autolink.js +++ b/packages/ckeditor5-link/src/autolink.js @@ -8,6 +8,9 @@ */ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import TextWatcher from '@ckeditor/ckeditor5-typing/src/textwatcher'; + +const regexp = /(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)) $/; /** * The auto link plugin. @@ -21,4 +24,39 @@ export default class AutoLink extends Plugin { static get pluginName() { return 'AutoLink'; } + + /** + * @inheritDoc + */ + init() { + const editor = this.editor; + + const watcher = new TextWatcher( editor.model, text => { + const match = regexp.exec( text ); + + if ( match ) { + return { match }; + } + } ); + + const input = editor.plugins.get( 'Input' ); + + watcher.on( 'matched:data', ( evt, data ) => { + const { batch, range, match } = data; + + if ( !input.isInput( batch ) ) { + return; + } + + const url = match[ 1 ]; + + // Enqueue change to make undo step. + editor.model.enqueueChange( writer => { + writer.setAttribute( 'linkHref', url, writer.createRange( + range.start, + range.end.getShiftedBy( -1 ) + ) ); + } ); + } ); + } } diff --git a/packages/ckeditor5-link/tests/autolink.js b/packages/ckeditor5-link/tests/autolink.js index a9851aadb0d..d69f508bc9b 100644 --- a/packages/ckeditor5-link/tests/autolink.js +++ b/packages/ckeditor5-link/tests/autolink.js @@ -10,6 +10,7 @@ import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model import LinkEditing from '../src/linkediting'; import AutoLink from '../src/autolink'; +import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting'; describe( 'AutoLink', () => { it( 'should be named', () => { @@ -20,7 +21,7 @@ describe( 'AutoLink', () => { let editor; beforeEach( async () => { - editor = await ModelTestEditor.create( { plugins: [ Paragraph, Input, LinkEditing, AutoLink ] } ); + editor = await ModelTestEditor.create( { plugins: [ Paragraph, Input, LinkEditing, AutoLink, UndoEditing ] } ); setData( editor.model, '[]' ); } ); From e72744f5255434544e00558e1eefa9e3962f9fbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 18 Jun 2020 14:27:04 +0200 Subject: [PATCH 05/30] AutoLink should work inside paragraphs. --- packages/ckeditor5-link/src/autolink.js | 12 +++++++++--- packages/ckeditor5-link/tests/autolink.js | 22 +++++++++++++++++----- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/packages/ckeditor5-link/src/autolink.js b/packages/ckeditor5-link/src/autolink.js index e4415360829..32395598c77 100644 --- a/packages/ckeditor5-link/src/autolink.js +++ b/packages/ckeditor5-link/src/autolink.js @@ -32,6 +32,9 @@ export default class AutoLink extends Plugin { const editor = this.editor; const watcher = new TextWatcher( editor.model, text => { + // TODO - should be 2-step: + // 1. Detect "space" or "enter". + // 2. Check text before "space" or "enter". const match = regexp.exec( text ); if ( match ) { @@ -52,10 +55,13 @@ export default class AutoLink extends Plugin { // Enqueue change to make undo step. editor.model.enqueueChange( writer => { - writer.setAttribute( 'linkHref', url, writer.createRange( - range.start, + const linkRange = writer.createRange( + range.end.getShiftedBy( -( 1 + url.length ) ), range.end.getShiftedBy( -1 ) - ) ); + ); + + // TODO: use command for decorators support. + writer.setAttribute( 'linkHref', url, linkRange ); } ); } ); } diff --git a/packages/ckeditor5-link/tests/autolink.js b/packages/ckeditor5-link/tests/autolink.js index d69f508bc9b..7a924cae149 100644 --- a/packages/ckeditor5-link/tests/autolink.js +++ b/packages/ckeditor5-link/tests/autolink.js @@ -18,18 +18,20 @@ describe( 'AutoLink', () => { } ); describe( 'auto link behavior', () => { - let editor; + let editor, model; beforeEach( async () => { editor = await ModelTestEditor.create( { plugins: [ Paragraph, Input, LinkEditing, AutoLink, UndoEditing ] } ); - setData( editor.model, '[]' ); + model = editor.model; + + setData( model, '[]' ); } ); it( 'does not add linkHref attribute to a text link while typing', () => { simulateTyping( 'https://www.cksource.com' ); - expect( getData( editor.model ) ).to.equal( + expect( getData( model ) ).to.equal( 'https://www.cksource.com[]' ); } ); @@ -37,17 +39,27 @@ describe( 'AutoLink', () => { it( 'adds linkHref attribute to a text link after space', () => { simulateTyping( 'https://www.cksource.com ' ); - expect( getData( editor.model ) ).to.equal( + expect( getData( model ) ).to.equal( '<$text linkHref="https://www.cksource.com">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( 'can undo auto-linking', () => { simulateTyping( 'https://www.cksource.com ' ); editor.commands.execute( 'undo' ); - expect( getData( editor.model ) ).to.equal( + expect( getData( model ) ).to.equal( 'https://www.cksource.com []' ); } ); From 39ced1d88c0c66b588e09451365c4292aef2bcfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 18 Jun 2020 14:54:50 +0200 Subject: [PATCH 06/30] Add manual tests for the AutoLink feature. --- .../ckeditor5-link/tests/manual/autolink.html | 3 ++ .../ckeditor5-link/tests/manual/autolink.js | 29 +++++++++++++++++++ .../ckeditor5-link/tests/manual/autolink.md | 24 +++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 packages/ckeditor5-link/tests/manual/autolink.html create mode 100644 packages/ckeditor5-link/tests/manual/autolink.js create mode 100644 packages/ckeditor5-link/tests/manual/autolink.md diff --git a/packages/ckeditor5-link/tests/manual/autolink.html b/packages/ckeditor5-link/tests/manual/autolink.html new file mode 100644 index 00000000000..af8df35b43d --- /dev/null +++ b/packages/ckeditor5-link/tests/manual/autolink.html @@ -0,0 +1,3 @@ +
+

This is CKEditor5 from CKSource.

+
diff --git a/packages/ckeditor5-link/tests/manual/autolink.js b/packages/ckeditor5-link/tests/manual/autolink.js new file mode 100644 index 00000000000..e5402c7a9be --- /dev/null +++ b/packages/ckeditor5-link/tests/manual/autolink.js @@ -0,0 +1,29 @@ +/** + * @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:false, 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'; + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ Link, AutoLink, Typing, Paragraph, Undo, Enter, ShiftEnter ], + 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..4844b1965fb --- /dev/null +++ b/packages/ckeditor5-link/tests/manual/autolink.md @@ -0,0 +1,24 @@ +## AutoLink feature + +### After a space + +1. Type a URL: + - Staring with `http://`. + - staring with `https://`. + - staring without a protocol (www.cksource.com). +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. From df44897aeed03ae965b6391de794fc8cdabe3843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 18 Jun 2020 16:21:57 +0200 Subject: [PATCH 07/30] Execute auto link on enter and shiftEnter commands. --- packages/ckeditor5-link/src/autolink.js | 67 +++++++++++++++++++---- packages/ckeditor5-link/tests/autolink.js | 44 ++++++++++++++- 2 files changed, 98 insertions(+), 13 deletions(-) diff --git a/packages/ckeditor5-link/src/autolink.js b/packages/ckeditor5-link/src/autolink.js index 32395598c77..8b93214c740 100644 --- a/packages/ckeditor5-link/src/autolink.js +++ b/packages/ckeditor5-link/src/autolink.js @@ -9,8 +9,10 @@ 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 regexp = /(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)) $/; +const regExpII = /(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*))$/; /** * The auto link plugin. @@ -51,18 +53,61 @@ export default class AutoLink extends Plugin { return; } - const url = match[ 1 ]; + applyAutoLink( match[ 1 ], range, editor ); + } ); + } - // Enqueue change to make undo step. - editor.model.enqueueChange( writer => { - const linkRange = writer.createRange( - range.end.getShiftedBy( -( 1 + url.length ) ), - range.end.getShiftedBy( -1 ) - ); + /** + * @inheritDoc + */ + afterInit() { + const editor = this.editor; - // TODO: use command for decorators support. - writer.setAttribute( 'linkHref', url, linkRange ); - } ); - } ); + const enterCommand = editor.commands.get( 'enter' ); + const shiftEnterCommand = editor.commands.get( 'shiftEnter' ); + + shiftEnterCommand.on( 'execute', () => { + const position = editor.model.document.selection.getFirstPosition(); + + const rangeToCheck = editor.model.createRange( + editor.model.createPositionAt( position.parent, 0 ), + position.getShiftedBy( -1 ) + ); + + checkAndApplyAutoLinkOnRange( rangeToCheck, editor ); + }, { priority: 'low' } ); + + enterCommand.on( 'execute', () => { + const position = editor.model.document.selection.getFirstPosition(); + + const rangeToCheck = editor.model.createRange( + editor.model.createPositionAt( position.parent.previousSibling, 0 ), + editor.model.createPositionAt( position.parent.previousSibling, 'end' ) + ); + + checkAndApplyAutoLinkOnRange( rangeToCheck, editor ); + }, { priority: 'low' } ); + } +} + +function applyAutoLink( linkHref, range, editor, additionalOffset = 1 ) { + // Enqueue change to make undo step. + editor.model.enqueueChange( writer => { + const linkRange = writer.createRange( + range.end.getShiftedBy( -( additionalOffset + linkHref.length ) ), + range.end.getShiftedBy( -additionalOffset ) + ); + + writer.setAttribute( 'linkHref', linkHref, linkRange ); + } ); +} + +function checkAndApplyAutoLinkOnRange( rangeToCheck, editor ) { + const { text, range } = getLastTextLine( rangeToCheck, editor.model ); + + const match = regExpII.exec( text ); + + if ( match ) { + applyAutoLink( match[ 1 ], range, editor, 0 ); } } diff --git a/packages/ckeditor5-link/tests/autolink.js b/packages/ckeditor5-link/tests/autolink.js index 7a924cae149..5a94dcfda02 100644 --- a/packages/ckeditor5-link/tests/autolink.js +++ b/packages/ckeditor5-link/tests/autolink.js @@ -4,13 +4,15 @@ */ import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; +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'; -import UndoEditing from '@ckeditor/ckeditor5-undo/src/undoediting'; describe( 'AutoLink', () => { it( 'should be named', () => { @@ -21,7 +23,9 @@ describe( 'AutoLink', () => { let editor, model; beforeEach( async () => { - editor = await ModelTestEditor.create( { plugins: [ Paragraph, Input, LinkEditing, AutoLink, UndoEditing ] } ); + editor = await ModelTestEditor.create( { + plugins: [ Paragraph, Input, LinkEditing, AutoLink, UndoEditing, Enter, ShiftEnter ] + } ); model = editor.model; @@ -54,6 +58,42 @@ describe( 'AutoLink', () => { ); } ); + it( 'adds linkHref attribute to a text link after a soft break', () => { + setData( model, 'https://www.cksource.com[]' ); + + editor.execute( 'shiftEnter' ); + + expect( getData( model ) ).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( 'can undo auto-linking', () => { simulateTyping( 'https://www.cksource.com ' ); From 80ca5d51874df21aff8a07de9ede4b1f68e46270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 18 Jun 2020 16:40:53 +0200 Subject: [PATCH 08/30] Rewrite AutoLink + ShiftEnter integration due to bug. --- packages/ckeditor5-link/tests/autolink.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-link/tests/autolink.js b/packages/ckeditor5-link/tests/autolink.js index 5a94dcfda02..b2e6c2b5eab 100644 --- a/packages/ckeditor5-link/tests/autolink.js +++ b/packages/ckeditor5-link/tests/autolink.js @@ -63,10 +63,11 @@ describe( 'AutoLink', () => { editor.execute( 'shiftEnter' ); - expect( getData( model ) ).to.equal( + // 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' + - '[]' + + '' + '' ); } ); From c3a69ac59238db98f31603eecff1a84c63d55abb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 18 Jun 2020 16:58:23 +0200 Subject: [PATCH 09/30] Remove redundant RegExp from auto link. --- packages/ckeditor5-link/src/autolink.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/ckeditor5-link/src/autolink.js b/packages/ckeditor5-link/src/autolink.js index 8b93214c740..e1afc77df93 100644 --- a/packages/ckeditor5-link/src/autolink.js +++ b/packages/ckeditor5-link/src/autolink.js @@ -11,8 +11,13 @@ 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 regexp = /(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)) $/; -const regExpII = /(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*))$/; +const MIN_LINK_LENGTH_WITH_SPACE_AT_END = 4; // Ie: "t.co " (length 5). + +const urlRegExp = /(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*))$/; + +function isSingleSpaceAtTheEnd( text ) { + return text.length > MIN_LINK_LENGTH_WITH_SPACE_AT_END && text[ text.length - 1 ] === ' ' && text[ text.length - 2 ] !== ' '; +} /** * The auto link plugin. @@ -34,10 +39,13 @@ export default class AutoLink extends Plugin { const editor = this.editor; const watcher = new TextWatcher( editor.model, text => { - // TODO - should be 2-step: - // 1. Detect "space" or "enter". + // 1. Detect "space" after a text with a potential link. + if ( !isSingleSpaceAtTheEnd( text ) ) { + return; + } + // 2. Check text before "space" or "enter". - const match = regexp.exec( text ); + const match = urlRegExp.exec( text.substr( 0, text.length - 1 ) ); if ( match ) { return { match }; @@ -105,7 +113,7 @@ function applyAutoLink( linkHref, range, editor, additionalOffset = 1 ) { function checkAndApplyAutoLinkOnRange( rangeToCheck, editor ) { const { text, range } = getLastTextLine( rangeToCheck, editor.model ); - const match = regExpII.exec( text ); + const match = urlRegExp.exec( text ); if ( match ) { applyAutoLink( match[ 1 ], range, editor, 0 ); From c9b8642ba28c19056419d15269b01247c1c81635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 19 Jun 2020 09:33:03 +0200 Subject: [PATCH 10/30] Refactor AutoLink internals. --- packages/ckeditor5-link/src/autolink.js | 87 ++++++++++++++----------- 1 file changed, 50 insertions(+), 37 deletions(-) diff --git a/packages/ckeditor5-link/src/autolink.js b/packages/ckeditor5-link/src/autolink.js index e1afc77df93..8f1d20f0e9e 100644 --- a/packages/ckeditor5-link/src/autolink.js +++ b/packages/ckeditor5-link/src/autolink.js @@ -15,10 +15,6 @@ const MIN_LINK_LENGTH_WITH_SPACE_AT_END = 4; // Ie: "t.co " (length 5). const urlRegExp = /(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*))$/; -function isSingleSpaceAtTheEnd( text ) { - return text.length > MIN_LINK_LENGTH_WITH_SPACE_AT_END && text[ text.length - 1 ] === ' ' && text[ text.length - 2 ] !== ' '; -} - /** * The auto link plugin. * @@ -61,61 +57,78 @@ export default class AutoLink extends Plugin { return; } - applyAutoLink( match[ 1 ], range, editor ); + this._applyAutoLink( match[ 1 ], range ); } ); + + // todo: watcher.bind(); } /** * @inheritDoc */ afterInit() { - const editor = this.editor; + this._enableEnterHandling(); + this._enableShiftEnterHandling(); + } + _enableEnterHandling() { + const editor = this.editor; + const model = editor.model; const enterCommand = editor.commands.get( 'enter' ); - const shiftEnterCommand = editor.commands.get( 'shiftEnter' ); - shiftEnterCommand.on( 'execute', () => { - const position = editor.model.document.selection.getFirstPosition(); + enterCommand.on( 'execute', () => { + const position = model.document.selection.getFirstPosition(); - const rangeToCheck = editor.model.createRange( - editor.model.createPositionAt( position.parent, 0 ), - position.getShiftedBy( -1 ) + const rangeToCheck = model.createRange( + model.createPositionAt( position.parent.previousSibling, 0 ), + model.createPositionAt( position.parent.previousSibling, 'end' ) ); - checkAndApplyAutoLinkOnRange( rangeToCheck, editor ); - }, { priority: 'low' } ); + this._checkAndApplyAutoLinkOnRange( rangeToCheck ); + } ); + } - enterCommand.on( 'execute', () => { - const position = editor.model.document.selection.getFirstPosition(); + _enableShiftEnterHandling() { + const editor = this.editor; + const model = editor.model; - const rangeToCheck = editor.model.createRange( - editor.model.createPositionAt( position.parent.previousSibling, 0 ), - editor.model.createPositionAt( position.parent.previousSibling, 'end' ) + const shiftEnterCommand = editor.commands.get( 'shiftEnter' ); + + shiftEnterCommand.on( 'execute', () => { + const position = model.document.selection.getFirstPosition(); + + const rangeToCheck = model.createRange( + model.createPositionAt( position.parent, 0 ), + position.getShiftedBy( -1 ) ); - checkAndApplyAutoLinkOnRange( rangeToCheck, editor ); - }, { priority: 'low' } ); + this._checkAndApplyAutoLinkOnRange( rangeToCheck ); + } ); } -} -function applyAutoLink( linkHref, range, editor, additionalOffset = 1 ) { - // Enqueue change to make undo step. - editor.model.enqueueChange( writer => { - const linkRange = writer.createRange( - range.end.getShiftedBy( -( additionalOffset + linkHref.length ) ), - range.end.getShiftedBy( -additionalOffset ) - ); + _checkAndApplyAutoLinkOnRange( rangeToCheck ) { + const { text, range } = getLastTextLine( rangeToCheck, this.editor.model ); - writer.setAttribute( 'linkHref', linkHref, linkRange ); - } ); -} + const match = urlRegExp.exec( text ); -function checkAndApplyAutoLinkOnRange( rangeToCheck, editor ) { - const { text, range } = getLastTextLine( rangeToCheck, editor.model ); + if ( match ) { + this._applyAutoLink( match[ 1 ], range, 0 ); + } + } - const match = urlRegExp.exec( text ); + _applyAutoLink( linkHref, range, additionalOffset = 1 ) { + // Enqueue change to make undo step. + this.editor.model.enqueueChange( writer => { + const linkRange = writer.createRange( + range.end.getShiftedBy( -( additionalOffset + linkHref.length ) ), + range.end.getShiftedBy( -additionalOffset ) + ); - if ( match ) { - applyAutoLink( match[ 1 ], range, editor, 0 ); + writer.setAttribute( 'linkHref', linkHref, linkRange ); + } ); } } + +function isSingleSpaceAtTheEnd( text ) { + return text.length > MIN_LINK_LENGTH_WITH_SPACE_AT_END && text[ text.length - 1 ] === ' ' && text[ text.length - 2 ] !== ' '; +} From a283f19ffc916e4116147f734d057e7dc6971bfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 22 Jun 2020 09:25:31 +0200 Subject: [PATCH 11/30] Add tests for Undo + AutoLink. --- packages/ckeditor5-link/tests/autolink.js | 27 ++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-link/tests/autolink.js b/packages/ckeditor5-link/tests/autolink.js index b2e6c2b5eab..9afff9e4681 100644 --- a/packages/ckeditor5-link/tests/autolink.js +++ b/packages/ckeditor5-link/tests/autolink.js @@ -95,7 +95,7 @@ describe( 'AutoLink', () => { ); } ); - it( 'can undo auto-linking', () => { + it( 'can undo auto-linking (after space)', () => { simulateTyping( 'https://www.cksource.com ' ); editor.commands.execute( 'undo' ); @@ -105,6 +105,31 @@ describe( 'AutoLink', () => { ); } ); + it( 'can undo auto-linking (after )', () => { + setData( model, 'https://www.cksource.com[]' ); + + editor.execute( 'shiftEnter' ); + + editor.commands.execute( 'undo' ); + + expect( getData( model ) ).to.equal( + 'https://www.cksource.com[]' + ); + } ); + + it( 'can undo auto-linking (after enter)', () => { + setData( model, 'https://www.cksource.com[]' ); + + editor.execute( 'enter' ); + + editor.commands.execute( 'undo' ); + + expect( getData( model ) ).to.equal( + 'https://www.cksource.com' + + '[]' + ); + } ); + function simulateTyping( text ) { const letters = text.split( '' ); From d6db7975ba01abc9f6e58f6c97e5d91415195fb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 22 Jun 2020 11:11:19 +0200 Subject: [PATCH 12/30] Validate RegExp used for URL & emails. --- packages/ckeditor5-link/src/autolink.js | 28 ++++++++++++-- packages/ckeditor5-link/tests/autolink.js | 47 +++++++++++++++++++++++ 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-link/src/autolink.js b/packages/ckeditor5-link/src/autolink.js index 8f1d20f0e9e..0ec60f8358b 100644 --- a/packages/ckeditor5-link/src/autolink.js +++ b/packages/ckeditor5-link/src/autolink.js @@ -13,7 +13,25 @@ import getLastTextLine from '@ckeditor/ckeditor5-typing/src/utils/getlasttextlin const MIN_LINK_LENGTH_WITH_SPACE_AT_END = 4; // Ie: "t.co " (length 5). -const urlRegExp = /(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*))$/; +const urlRegExp = new RegExp( + // Group 1: Line start or after a space. + '(^|\\s)' + // Match . + // Group 2: Full detected URL. + '(' + + // Group 3 + 4: Protocol + domain. + '(([a-z]{3,9}:(?:\\/\\/)?)(?:[\\w]+)?[a-z0-9.-]+|(?:www\\.|[\\w]+)[a-z0-9.-]+)' + + // Group 5: Optional path + query string + location. + '((?:\\/[+~%/.\\w\\-_]*)?\\??(?:[-+=&;%@.\\w_]*)#?(?:[.!/\\\\\\w]*))?' + + ')$', 'i' ); + +// Simplified email test - should be run over previously found URL. +const emailRegExp = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i; + +const URL_POSITION_IN_MATCH = 2; + +function isEmail( linkHref ) { + return emailRegExp.exec( linkHref ); +} /** * The auto link plugin. @@ -57,7 +75,7 @@ export default class AutoLink extends Plugin { return; } - this._applyAutoLink( match[ 1 ], range ); + this._applyAutoLink( match[ URL_POSITION_IN_MATCH ], range ); } ); // todo: watcher.bind(); @@ -112,7 +130,7 @@ export default class AutoLink extends Plugin { const match = urlRegExp.exec( text ); if ( match ) { - this._applyAutoLink( match[ 1 ], range, 0 ); + this._applyAutoLink( match[ URL_POSITION_IN_MATCH ], range, 0 ); } } @@ -124,7 +142,9 @@ export default class AutoLink extends Plugin { range.end.getShiftedBy( -additionalOffset ) ); - writer.setAttribute( 'linkHref', linkHref, linkRange ); + const linkHrefValue = isEmail( linkHref ) ? `mailto://${ linkHref }` : linkHref; + + writer.setAttribute( 'linkHref', linkHrefValue, linkRange ); } ); } } diff --git a/packages/ckeditor5-link/tests/autolink.js b/packages/ckeditor5-link/tests/autolink.js index 9afff9e4681..1b26f396d1d 100644 --- a/packages/ckeditor5-link/tests/autolink.js +++ b/packages/ckeditor5-link/tests/autolink.js @@ -130,6 +130,53 @@ describe( 'AutoLink', () => { ); } ); + 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 []' + ); + } ); + + const supportedURLs = [ + 'http://cksource.com', + 'https://cksource.com', + 'http://www.cksource.com', + 'hTtP://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://localhost', + 'ftp://cksource.com', + 'mailto://cksource@cksource.com', + 'www.cksource.com', + 'cksource.com' + ]; + + const unsupportedURLs = [ + 'http://www.cksource.com/some/path/index.html#abc?foo=bar', // Wrong #? sequence. + 'http:/cksource.com' + ]; + + for ( const supportedURL of supportedURLs ) { + it( `should detect "${ supportedURL }" as a valid URL`, () => { + simulateTyping( supportedURL + ' ' ); + + expect( getData( model ) ).to.equal( + `<$text linkHref="${ supportedURL }">${ supportedURL } []` ); + } ); + } + + for ( const unsupportedURL of unsupportedURLs ) { + it( `should not detect "${ unsupportedURL }" as a valid URL`, () => { + simulateTyping( unsupportedURL + ' ' ); + + expect( getData( model ) ).to.equal( + `${ unsupportedURL } []` ); + } ); + } + function simulateTyping( text ) { const letters = text.split( '' ); From 2cdaba8da883467d55727bf83a882bcac9415765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 22 Jun 2020 11:15:47 +0200 Subject: [PATCH 13/30] Refactor auto link URL retrieval. --- packages/ckeditor5-link/src/autolink.js | 34 +++++++++++++++---------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/packages/ckeditor5-link/src/autolink.js b/packages/ckeditor5-link/src/autolink.js index 0ec60f8358b..d02cedfc5f2 100644 --- a/packages/ckeditor5-link/src/autolink.js +++ b/packages/ckeditor5-link/src/autolink.js @@ -24,15 +24,11 @@ const urlRegExp = new RegExp( '((?:\\/[+~%/.\\w\\-_]*)?\\??(?:[-+=&;%@.\\w_]*)#?(?:[.!/\\\\\\w]*))?' + ')$', 'i' ); +const URL_GROUP_IN_MATCH = 2; + // Simplified email test - should be run over previously found URL. const emailRegExp = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i; -const URL_POSITION_IN_MATCH = 2; - -function isEmail( linkHref ) { - return emailRegExp.exec( linkHref ); -} - /** * The auto link plugin. * @@ -59,23 +55,23 @@ export default class AutoLink extends Plugin { } // 2. Check text before "space" or "enter". - const match = urlRegExp.exec( text.substr( 0, text.length - 1 ) ); + const url = getUrlAtTextEnd( text.substr( 0, text.length - 1 ) ); - if ( match ) { - return { match }; + if ( url ) { + return { url }; } } ); const input = editor.plugins.get( 'Input' ); watcher.on( 'matched:data', ( evt, data ) => { - const { batch, range, match } = data; + const { batch, range, url } = data; if ( !input.isInput( batch ) ) { return; } - this._applyAutoLink( match[ URL_POSITION_IN_MATCH ], range ); + this._applyAutoLink( url, range ); } ); // todo: watcher.bind(); @@ -127,10 +123,10 @@ export default class AutoLink extends Plugin { _checkAndApplyAutoLinkOnRange( rangeToCheck ) { const { text, range } = getLastTextLine( rangeToCheck, this.editor.model ); - const match = urlRegExp.exec( text ); + const url = getUrlAtTextEnd( text ); - if ( match ) { - this._applyAutoLink( match[ URL_POSITION_IN_MATCH ], range, 0 ); + if ( url ) { + this._applyAutoLink( url, range, 0 ); } } @@ -152,3 +148,13 @@ export default class AutoLink extends Plugin { 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 = urlRegExp.exec( text ); + + return match ? match[ URL_GROUP_IN_MATCH ] : null; +} + +function isEmail( linkHref ) { + return emailRegExp.exec( linkHref ); +} From 7c560ef5b0aa7ebc51a5a2db852ad0d2d194acfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 22 Jun 2020 14:05:52 +0200 Subject: [PATCH 14/30] Add AutoLink feature guide. --- .../docs/_snippets/features/autolink.html | 4 ++++ .../docs/_snippets/features/autolink.js | 23 +++++++++++++++++++ .../_snippets/features/build-link-source.js | 5 ++++ packages/ckeditor5-link/docs/features/link.md | 21 +++++++++++++++++ 4 files changed, 53 insertions(+) create mode 100644 packages/ckeditor5-link/docs/_snippets/features/autolink.html create mode 100644 packages/ckeditor5-link/docs/_snippets/features/autolink.js 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 From 7ec01c3e21cf6d5f405f20e0391312cfd5a0e460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 22 Jun 2020 14:28:01 +0200 Subject: [PATCH 15/30] Fix code style for RegExps constants in AutoLink feature. --- packages/ckeditor5-link/src/autolink.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-link/src/autolink.js b/packages/ckeditor5-link/src/autolink.js index d02cedfc5f2..1b95ea6a6e1 100644 --- a/packages/ckeditor5-link/src/autolink.js +++ b/packages/ckeditor5-link/src/autolink.js @@ -13,7 +13,7 @@ import getLastTextLine from '@ckeditor/ckeditor5-typing/src/utils/getlasttextlin const MIN_LINK_LENGTH_WITH_SPACE_AT_END = 4; // Ie: "t.co " (length 5). -const urlRegExp = new RegExp( +const URL_REG_EXP = new RegExp( // Group 1: Line start or after a space. '(^|\\s)' + // Match . // Group 2: Full detected URL. @@ -27,7 +27,7 @@ const urlRegExp = new RegExp( const URL_GROUP_IN_MATCH = 2; // Simplified email test - should be run over previously found URL. -const emailRegExp = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i; +const EMAIL_REG_EXP = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i; /** * The auto link plugin. @@ -150,11 +150,11 @@ function isSingleSpaceAtTheEnd( text ) { } function getUrlAtTextEnd( text ) { - const match = urlRegExp.exec( text ); + const match = URL_REG_EXP.exec( text ); return match ? match[ URL_GROUP_IN_MATCH ] : null; } function isEmail( linkHref ) { - return emailRegExp.exec( linkHref ); + return EMAIL_REG_EXP.exec( linkHref ); } From d6ec3a7ad0ce673d461c29e8a46b9d011149d796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 22 Jun 2020 14:45:50 +0200 Subject: [PATCH 16/30] Move auto link undo test to own integration suite. --- packages/ckeditor5-link/src/autolink.js | 2 +- packages/ckeditor5-link/tests/autolink.js | 98 +++++++++++++---------- 2 files changed, 56 insertions(+), 44 deletions(-) diff --git a/packages/ckeditor5-link/src/autolink.js b/packages/ckeditor5-link/src/autolink.js index 1b95ea6a6e1..3a9e2cec077 100644 --- a/packages/ckeditor5-link/src/autolink.js +++ b/packages/ckeditor5-link/src/autolink.js @@ -74,7 +74,7 @@ export default class AutoLink extends Plugin { this._applyAutoLink( url, range ); } ); - // todo: watcher.bind(); + watcher.bind( 'isEnabled' ).to( this ); } /** diff --git a/packages/ckeditor5-link/tests/autolink.js b/packages/ckeditor5-link/tests/autolink.js index 1b26f396d1d..a9149c50225 100644 --- a/packages/ckeditor5-link/tests/autolink.js +++ b/packages/ckeditor5-link/tests/autolink.js @@ -15,16 +15,18 @@ import LinkEditing from '../src/linkediting'; import AutoLink from '../src/autolink'; describe( 'AutoLink', () => { + let editor; + it( 'should be named', () => { expect( AutoLink.pluginName ).to.equal( 'AutoLink' ); } ); describe( 'auto link behavior', () => { - let editor, model; + let model; beforeEach( async () => { editor = await ModelTestEditor.create( { - plugins: [ Paragraph, Input, LinkEditing, AutoLink, UndoEditing, Enter, ShiftEnter ] + plugins: [ Paragraph, Input, LinkEditing, AutoLink, Enter, ShiftEnter ] } ); model = editor.model; @@ -95,41 +97,6 @@ describe( 'AutoLink', () => { ); } ); - it( 'can undo auto-linking (after space)', () => { - simulateTyping( 'https://www.cksource.com ' ); - - editor.commands.execute( 'undo' ); - - expect( getData( model ) ).to.equal( - 'https://www.cksource.com []' - ); - } ); - - it( 'can undo auto-linking (after )', () => { - setData( model, 'https://www.cksource.com[]' ); - - editor.execute( 'shiftEnter' ); - - editor.commands.execute( 'undo' ); - - expect( getData( model ) ).to.equal( - 'https://www.cksource.com[]' - ); - } ); - - it( 'can undo auto-linking (after enter)', () => { - setData( model, 'https://www.cksource.com[]' ); - - editor.execute( 'enter' ); - - editor.commands.execute( 'undo' ); - - expect( getData( model ) ).to.equal( - 'https://www.cksource.com' + - '[]' - ); - } ); - it( 'adds "mailto://" to link of detected email addresses', () => { simulateTyping( 'newsletter@cksource.com ' ); @@ -176,13 +143,58 @@ describe( 'AutoLink', () => { `${ unsupportedURL } []` ); } ); } + } ); - function simulateTyping( text ) { - const letters = text.split( '' ); + describe( 'Undo integration', () => { + let model; - for ( const letter of letters ) { - editor.execute( 'input', { text: letter } ); - } - } + beforeEach( async () => { + editor = await ModelTestEditor.create( { + plugins: [ Paragraph, Input, LinkEditing, AutoLink, UndoEditing, Enter, ShiftEnter ] + } ); + + 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' + + '[]' + ); + } ); } ); + + function simulateTyping( text ) { + const letters = text.split( '' ); + + for ( const letter of letters ) { + editor.execute( 'input', { text: letter } ); + } + } } ); From f4231444a35d6d79417a78fbe5ca04161c776bdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 22 Jun 2020 14:50:31 +0200 Subject: [PATCH 17/30] Disable AutoLink in code block. --- packages/ckeditor5-link/src/autolink.js | 6 +++++ packages/ckeditor5-link/tests/autolink.js | 29 +++++++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-link/src/autolink.js b/packages/ckeditor5-link/src/autolink.js index 3a9e2cec077..77a98227f0a 100644 --- a/packages/ckeditor5-link/src/autolink.js +++ b/packages/ckeditor5-link/src/autolink.js @@ -47,6 +47,12 @@ export default class AutoLink extends Plugin { */ 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' ); + } ); const watcher = new TextWatcher( editor.model, text => { // 1. Detect "space" after a text with a potential link. diff --git a/packages/ckeditor5-link/tests/autolink.js b/packages/ckeditor5-link/tests/autolink.js index a9149c50225..485c31fbc28 100644 --- a/packages/ckeditor5-link/tests/autolink.js +++ b/packages/ckeditor5-link/tests/autolink.js @@ -4,6 +4,7 @@ */ 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'; @@ -26,7 +27,7 @@ describe( 'AutoLink', () => { beforeEach( async () => { editor = await ModelTestEditor.create( { - plugins: [ Paragraph, Input, LinkEditing, AutoLink, Enter, ShiftEnter ] + plugins: [ Paragraph, Input, Enter, ShiftEnter, LinkEditing, AutoLink ] } ); model = editor.model; @@ -150,7 +151,7 @@ describe( 'AutoLink', () => { beforeEach( async () => { editor = await ModelTestEditor.create( { - plugins: [ Paragraph, Input, LinkEditing, AutoLink, UndoEditing, Enter, ShiftEnter ] + plugins: [ Paragraph, Input, Enter, ShiftEnter, LinkEditing, AutoLink, UndoEditing ] } ); model = editor.model; @@ -190,6 +191,30 @@ describe( 'AutoLink', () => { } ); } ); + 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', () => { + 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' ); + } ); + } ); + function simulateTyping( text ) { const letters = text.split( '' ); From 4860012dec2608794d843e596113d0f3c256eeaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 22 Jun 2020 14:51:43 +0200 Subject: [PATCH 18/30] Refactor typing integration in autolink. --- packages/ckeditor5-link/src/autolink.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/ckeditor5-link/src/autolink.js b/packages/ckeditor5-link/src/autolink.js index 77a98227f0a..951c0d5be18 100644 --- a/packages/ckeditor5-link/src/autolink.js +++ b/packages/ckeditor5-link/src/autolink.js @@ -54,6 +54,20 @@ export default class AutoLink extends Plugin { this.isEnabled = !selection.anchor.parent.is( 'codeBlock' ); } ); + this._enableTypingHandling(); + } + + /** + * @inheritDoc + */ + afterInit() { + this._enableEnterHandling(); + this._enableShiftEnterHandling(); + } + + _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 ) ) { @@ -83,14 +97,6 @@ export default class AutoLink extends Plugin { watcher.bind( 'isEnabled' ).to( this ); } - /** - * @inheritDoc - */ - afterInit() { - this._enableEnterHandling(); - this._enableShiftEnterHandling(); - } - _enableEnterHandling() { const editor = this.editor; const model = editor.model; From 62aebec5e8832ecdba6490b8288051c9adb4989a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 22 Jun 2020 14:57:24 +0200 Subject: [PATCH 19/30] Update AutoLink API docs. --- packages/ckeditor5-link/src/autolink.js | 31 ++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-link/src/autolink.js b/packages/ckeditor5-link/src/autolink.js index 951c0d5be18..4cfaf490a69 100644 --- a/packages/ckeditor5-link/src/autolink.js +++ b/packages/ckeditor5-link/src/autolink.js @@ -65,6 +65,11 @@ export default class AutoLink extends Plugin { this._enableShiftEnterHandling(); } + /** + * Enables auto-link on typing. + * + * @private + */ _enableTypingHandling() { const editor = this.editor; @@ -74,7 +79,7 @@ export default class AutoLink extends Plugin { return; } - // 2. Check text before "space" or "enter". + // 2. Check text before last typed "space". const url = getUrlAtTextEnd( text.substr( 0, text.length - 1 ) ); if ( url ) { @@ -97,6 +102,11 @@ export default class AutoLink extends Plugin { watcher.bind( 'isEnabled' ).to( this ); } + /** + * Enables auto-link on enter key. + * + * @private + */ _enableEnterHandling() { const editor = this.editor; const model = editor.model; @@ -114,6 +124,11 @@ export default class AutoLink extends Plugin { } ); } + /** + * Enables auto-link on shift+enter key. + * + * @private + */ _enableShiftEnterHandling() { const editor = this.editor; const model = editor.model; @@ -132,6 +147,12 @@ export default class AutoLink extends Plugin { } ); } + /** + * Checks passed range if it contains a linkable text. + * + * @param {module:engine/model/range~Range} rangeToCheck + * @private + */ _checkAndApplyAutoLinkOnRange( rangeToCheck ) { const { text, range } = getLastTextLine( rangeToCheck, this.editor.model ); @@ -142,6 +163,13 @@ export default class AutoLink extends Plugin { } } + /** + * Applies link on selected renage. + * + * @param {String} linkHref Link href value. + * @param {module:engine/model/range~Range} range Text range to apply link attribute. + * @private + */ _applyAutoLink( linkHref, range, additionalOffset = 1 ) { // Enqueue change to make undo step. this.editor.model.enqueueChange( writer => { @@ -157,6 +185,7 @@ export default class AutoLink extends Plugin { } } +// 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 ] !== ' '; } From 64175536ee5442296a18dbd914c6361670cd8424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 22 Jun 2020 15:05:11 +0200 Subject: [PATCH 20/30] Fix code style in AutoLink internal code. --- packages/ckeditor5-link/src/autolink.js | 32 +++++++++++++++---------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/packages/ckeditor5-link/src/autolink.js b/packages/ckeditor5-link/src/autolink.js index 4cfaf490a69..95b12b07f7a 100644 --- a/packages/ckeditor5-link/src/autolink.js +++ b/packages/ckeditor5-link/src/autolink.js @@ -96,7 +96,12 @@ export default class AutoLink extends Plugin { return; } - this._applyAutoLink( url, range ); + const linkRange = editor.model.createRange( + range.end.getShiftedBy( -( 1 + url.length ) ), + range.end.getShiftedBy( -1 ) + ); + + this._applyAutoLink( url, linkRange ); } ); watcher.bind( 'isEnabled' ).to( this ); @@ -154,33 +159,34 @@ export default class AutoLink extends Plugin { * @private */ _checkAndApplyAutoLinkOnRange( rangeToCheck ) { - const { text, range } = getLastTextLine( rangeToCheck, this.editor.model ); + const model = this.editor.model; + const { text, range } = getLastTextLine( rangeToCheck, model ); const url = getUrlAtTextEnd( text ); if ( url ) { - this._applyAutoLink( url, range, 0 ); + const linkRange = model.createRange( + range.end.getShiftedBy( -url.length ), + range.end + ); + + this._applyAutoLink( url, linkRange ); } } /** - * Applies link on selected renage. + * Applies link on a given range. * - * @param {String} linkHref Link href value. + * @param {String} url URL to link. * @param {module:engine/model/range~Range} range Text range to apply link attribute. * @private */ - _applyAutoLink( linkHref, range, additionalOffset = 1 ) { + _applyAutoLink( url, range ) { // Enqueue change to make undo step. this.editor.model.enqueueChange( writer => { - const linkRange = writer.createRange( - range.end.getShiftedBy( -( additionalOffset + linkHref.length ) ), - range.end.getShiftedBy( -additionalOffset ) - ); - - const linkHrefValue = isEmail( linkHref ) ? `mailto://${ linkHref }` : linkHref; + const linkHrefValue = isEmail( url ) ? `mailto://${ url }` : url; - writer.setAttribute( 'linkHref', linkHrefValue, linkRange ); + writer.setAttribute( 'linkHref', linkHrefValue, range ); } ); } } From 2e34c1b2bf014a402f115c2a15c343e301fb0092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 22 Jun 2020 15:08:23 +0200 Subject: [PATCH 21/30] Add info in manual tests about e-mail linking. --- packages/ckeditor5-link/tests/manual/autolink.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ckeditor5-link/tests/manual/autolink.md b/packages/ckeditor5-link/tests/manual/autolink.md index 4844b1965fb..edded8bf0e6 100644 --- a/packages/ckeditor5-link/tests/manual/autolink.md +++ b/packages/ckeditor5-link/tests/manual/autolink.md @@ -6,6 +6,7 @@ - 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. From 967aac5a1017a42aca806bd97a40d73660ad6047 Mon Sep 17 00:00:00 2001 From: Maciej Date: Wed, 24 Jun 2020 06:38:32 +0200 Subject: [PATCH 22/30] Update packages/ckeditor5-link/tests/manual/autolink.js Co-authored-by: Kamil Piechaczek --- packages/ckeditor5-link/tests/manual/autolink.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-link/tests/manual/autolink.js b/packages/ckeditor5-link/tests/manual/autolink.js index e5402c7a9be..0cbfaf5cda7 100644 --- a/packages/ckeditor5-link/tests/manual/autolink.js +++ b/packages/ckeditor5-link/tests/manual/autolink.js @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -/* globals console:false, window, document */ +/* globals console, window, document */ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; From c2b508398804d01e8db9c8f41fb8290e493369fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 24 Jun 2020 06:39:46 +0200 Subject: [PATCH 23/30] Add missing dev dependency. --- packages/ckeditor5-link/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ckeditor5-link/package.json b/packages/ckeditor5-link/package.json index f556dbab7ab..fe634cfe97b 100644 --- a/packages/ckeditor5-link/package.json +++ b/packages/ckeditor5-link/package.json @@ -21,6 +21,7 @@ "@ckeditor/ckeditor5-basic-styles": "^19.0.1", "@ckeditor/ckeditor5-block-quote": "^19.0.1", "@ckeditor/ckeditor5-clipboard": "^19.0.1", + "@ckeditor/ckeditor5-code-block": "^19.0.1", "@ckeditor/ckeditor5-editor-classic": "^19.0.1", "@ckeditor/ckeditor5-enter": "^19.0.1", "@ckeditor/ckeditor5-paragraph": "^19.1.0", From e415b2ec2b858b9076216bf971dca08b6b754d21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 24 Jun 2020 06:42:14 +0200 Subject: [PATCH 24/30] Add typing test. --- packages/ckeditor5-link/tests/autolink.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/ckeditor5-link/tests/autolink.js b/packages/ckeditor5-link/tests/autolink.js index 485c31fbc28..f6dd03741c3 100644 --- a/packages/ckeditor5-link/tests/autolink.js +++ b/packages/ckeditor5-link/tests/autolink.js @@ -35,6 +35,14 @@ describe( 'AutoLink', () => { 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' ); From d6ddfe6ce6e997f0b191d5503510532905e201ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 24 Jun 2020 09:05:57 +0200 Subject: [PATCH 25/30] Improve RegExp for detecting URLs in typed text. --- packages/ckeditor5-link/src/autolink.js | 37 ++++- packages/ckeditor5-link/tests/autolink.js | 135 +++++++++++++----- .../ckeditor5-link/tests/manual/autolink.html | 10 +- .../ckeditor5-link/tests/manual/autolink.js | 3 +- 4 files changed, 139 insertions(+), 46 deletions(-) diff --git a/packages/ckeditor5-link/src/autolink.js b/packages/ckeditor5-link/src/autolink.js index 95b12b07f7a..6c622935a87 100644 --- a/packages/ckeditor5-link/src/autolink.js +++ b/packages/ckeditor5-link/src/autolink.js @@ -13,21 +13,44 @@ import getLastTextLine from '@ckeditor/ckeditor5-typing/src/utils/getlasttextlin 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)' + // Match . - // Group 2: Full detected URL. + '(^|\\s)' + + // Group 2: Detected URL (or e-mail). '(' + - // Group 3 + 4: Protocol + domain. - '(([a-z]{3,9}:(?:\\/\\/)?)(?:[\\w]+)?[a-z0-9.-]+|(?:www\\.|[\\w]+)[a-z0-9.-]+)' + - // Group 5: Optional path + query string + location. - '((?:\\/[+~%/.\\w\\-_]*)?\\??(?:[-+=&;%@.\\w_]*)#?(?:[.!/\\\\\\w]*))?' + + // 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 = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i; +const EMAIL_REG_EXP = /^[\S]+@((?![-_])(?:[-\w\u00a1-\uffff]{0,63}[^-_]\.))+(?:[a-z\u00a1-\uffff]{2,})$/i; /** * The auto link plugin. diff --git a/packages/ckeditor5-link/tests/autolink.js b/packages/ckeditor5-link/tests/autolink.js index f6dd03741c3..d5df008b040 100644 --- a/packages/ckeditor5-link/tests/autolink.js +++ b/packages/ckeditor5-link/tests/autolink.js @@ -114,44 +114,105 @@ describe( 'AutoLink', () => { ); } ); - const supportedURLs = [ - 'http://cksource.com', - 'https://cksource.com', - 'http://www.cksource.com', - 'hTtP://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://localhost', - 'ftp://cksource.com', - 'mailto://cksource@cksource.com', - 'www.cksource.com', - 'cksource.com' - ]; - - const unsupportedURLs = [ - 'http://www.cksource.com/some/path/index.html#abc?foo=bar', // Wrong #? sequence. - 'http:/cksource.com' - ]; - - for ( const supportedURL of supportedURLs ) { - it( `should detect "${ supportedURL }" as a valid URL`, () => { - simulateTyping( supportedURL + ' ' ); - - expect( getData( model ) ).to.equal( - `<$text linkHref="${ supportedURL }">${ supportedURL } []` ); - } ); - } - - for ( const unsupportedURL of unsupportedURLs ) { - it( `should not detect "${ unsupportedURL }" as a valid URL`, () => { - simulateTyping( unsupportedURL + ' ' ); + // 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 } []` ); + } ); + } + } ); - expect( getData( model ) ).to.equal( - `${ unsupportedURL } []` ); - } ); - } + 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', () => { diff --git a/packages/ckeditor5-link/tests/manual/autolink.html b/packages/ckeditor5-link/tests/manual/autolink.html index af8df35b43d..f1574ee36a0 100644 --- a/packages/ckeditor5-link/tests/manual/autolink.html +++ b/packages/ckeditor5-link/tests/manual/autolink.html @@ -1,3 +1,11 @@
-

This is CKEditor5 from CKSource.

+

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 index 0cbfaf5cda7..277ab6fcf17 100644 --- a/packages/ckeditor5-link/tests/manual/autolink.js +++ b/packages/ckeditor5-link/tests/manual/autolink.js @@ -15,10 +15,11 @@ 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: [ Link, AutoLink, Typing, Paragraph, Undo, Enter, ShiftEnter ], + plugins: [ Bold, Typing, Paragraph, Undo, Enter, ShiftEnter, Link, AutoLink ], toolbar: [ 'link', 'undo', 'redo' ] } ) .then( editor => { From a591093ad9337a22c2bbf00cf79aa0e60504b1ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 24 Jun 2020 09:12:49 +0200 Subject: [PATCH 26/30] AutoLink should work without Enter or ShiftEnter. --- packages/ckeditor5-link/src/autolink.js | 8 ++++++++ packages/ckeditor5-link/tests/autolink.js | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/packages/ckeditor5-link/src/autolink.js b/packages/ckeditor5-link/src/autolink.js index 6c622935a87..8319c039a87 100644 --- a/packages/ckeditor5-link/src/autolink.js +++ b/packages/ckeditor5-link/src/autolink.js @@ -140,6 +140,10 @@ export default class AutoLink extends Plugin { const model = editor.model; const enterCommand = editor.commands.get( 'enter' ); + if ( !enterCommand ) { + return; + } + enterCommand.on( 'execute', () => { const position = model.document.selection.getFirstPosition(); @@ -163,6 +167,10 @@ export default class AutoLink extends Plugin { const shiftEnterCommand = editor.commands.get( 'shiftEnter' ); + if ( !shiftEnterCommand ) { + return; + } + shiftEnterCommand.on( 'execute', () => { const position = model.document.selection.getFirstPosition(); diff --git a/packages/ckeditor5-link/tests/autolink.js b/packages/ckeditor5-link/tests/autolink.js index d5df008b040..29b8fe5b6d5 100644 --- a/packages/ckeditor5-link/tests/autolink.js +++ b/packages/ckeditor5-link/tests/autolink.js @@ -22,6 +22,14 @@ describe( 'AutoLink', () => { 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; From dfd5944a6123e406f1acf8363c8271e53e002c25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 24 Jun 2020 10:06:43 +0200 Subject: [PATCH 27/30] Disable auto-link where linkHref is not allowed. --- packages/ckeditor5-link/src/autolink.js | 18 ++++++++++++++---- packages/ckeditor5-link/tests/autolink.js | 10 ++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-link/src/autolink.js b/packages/ckeditor5-link/src/autolink.js index 8319c039a87..bcea622e18b 100644 --- a/packages/ckeditor5-link/src/autolink.js +++ b/packages/ckeditor5-link/src/autolink.js @@ -42,9 +42,9 @@ const URL_REG_EXP = new RegExp( '(www.|(\\S+@))' + // Host & domain names. '((?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.))+' + - // TLD identifier name. - '(?:[a-z\\u00a1-\\uffff]{2,})' + - ')' + + // TLD identifier name. + '(?:[a-z\\u00a1-\\uffff]{2,})' + + ')' + ')$', 'i' ); const URL_GROUP_IN_MATCH = 2; @@ -213,8 +213,14 @@ export default class AutoLink extends Plugin { * @private */ _applyAutoLink( url, range ) { + const model = this.editor.model; + + if ( !isLinkAllowedOnRange( range, model ) ) { + return; + } + // Enqueue change to make undo step. - this.editor.model.enqueueChange( writer => { + model.enqueueChange( writer => { const linkHrefValue = isEmail( url ) ? `mailto://${ url }` : url; writer.setAttribute( 'linkHref', linkHrefValue, range ); @@ -236,3 +242,7 @@ function getUrlAtTextEnd( text ) { 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 index 29b8fe5b6d5..ed23dca9adb 100644 --- a/packages/ckeditor5-link/tests/autolink.js +++ b/packages/ckeditor5-link/tests/autolink.js @@ -67,6 +67,16 @@ describe( 'AutoLink', () => { ); } ); + 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( 'adds linkHref attribute to a text link after space (inside paragraph)', () => { setData( model, 'Foo Bar [] Baz' ); From 37388ed21dedcb813ccf18a831aba0029f142efe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 24 Jun 2020 10:14:57 +0200 Subject: [PATCH 28/30] Add tests for enter and shift enter in code block. --- packages/ckeditor5-link/tests/autolink.js | 29 ++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-link/tests/autolink.js b/packages/ckeditor5-link/tests/autolink.js index ed23dca9adb..73fc0028d34 100644 --- a/packages/ckeditor5-link/tests/autolink.js +++ b/packages/ckeditor5-link/tests/autolink.js @@ -289,7 +289,7 @@ describe( 'AutoLink', () => { model = editor.model; } ); - it( 'should be disabled inside code blocks', () => { + it( 'should be disabled inside code blocks (on space)', () => { setData( model, 'some [] code' ); const plugin = editor.plugins.get( 'AutoLink' ); @@ -300,6 +300,33 @@ describe( 'AutoLink', () => { 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 ) { From 589c6c4abcb2802f496455928bc9529ed1132c64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 24 Jun 2020 10:15:07 +0200 Subject: [PATCH 29/30] Code style improvements. --- packages/ckeditor5-link/src/autolink.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-link/src/autolink.js b/packages/ckeditor5-link/src/autolink.js index bcea622e18b..f203a292f83 100644 --- a/packages/ckeditor5-link/src/autolink.js +++ b/packages/ckeditor5-link/src/autolink.js @@ -119,10 +119,10 @@ export default class AutoLink extends Plugin { return; } - const linkRange = editor.model.createRange( - range.end.getShiftedBy( -( 1 + url.length ) ), - range.end.getShiftedBy( -1 ) - ); + 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 ); } ); From 239de3d2e36e1d32713fb46f8ffd7be48424e6be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 24 Jun 2020 10:32:06 +0200 Subject: [PATCH 30/30] Add tests for force-disabling AutoLink. --- packages/ckeditor5-link/src/autolink.js | 2 +- packages/ckeditor5-link/tests/autolink.js | 39 +++++++++++++++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-link/src/autolink.js b/packages/ckeditor5-link/src/autolink.js index f203a292f83..0ae3df263a8 100644 --- a/packages/ckeditor5-link/src/autolink.js +++ b/packages/ckeditor5-link/src/autolink.js @@ -215,7 +215,7 @@ export default class AutoLink extends Plugin { _applyAutoLink( url, range ) { const model = this.editor.model; - if ( !isLinkAllowedOnRange( range, model ) ) { + if ( !this.isEnabled || !isLinkAllowedOnRange( range, model ) ) { return; } diff --git a/packages/ckeditor5-link/tests/autolink.js b/packages/ckeditor5-link/tests/autolink.js index 73fc0028d34..680d8430830 100644 --- a/packages/ckeditor5-link/tests/autolink.js +++ b/packages/ckeditor5-link/tests/autolink.js @@ -77,6 +77,39 @@ describe( 'AutoLink', () => { ); } ); + 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' ); @@ -87,7 +120,7 @@ describe( 'AutoLink', () => { ); } ); - it( 'adds linkHref attribute to a text link after a soft break', () => { + it( 'adds linkHref attribute to a text link on shift enter', () => { setData( model, 'https://www.cksource.com[]' ); editor.execute( 'shiftEnter' ); @@ -95,8 +128,8 @@ describe( 'AutoLink', () => { // 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' + - '' + + '<$text linkHref="https://www.cksource.com">https://www.cksource.com' + + '' + '' ); } );