Răsfoiți Sursa

Merge remote-tracking branch 'origin/master' into i/7492

Marek Lewandowski 5 ani în urmă
părinte
comite
e66f9f3f15

+ 13 - 0
docs/framework/guides/contributing/code-style.md

@@ -510,6 +510,19 @@ There are some special rules and tips for tests.
 
 
 	Think about this — when you fix a bug by adding a parameter to an existing function call you do not affect code coverage (that line was called anyway). However, you had a bug, meaning that your test suite did not cover it. Therefore, a test must be created for that code change.
 	Think about this — when you fix a bug by adding a parameter to an existing function call you do not affect code coverage (that line was called anyway). However, you had a bug, meaning that your test suite did not cover it. Therefore, a test must be created for that code change.
 * It should be `expect( x ).to.equal( y )`. **NOT**: ~~`expect( x ).to.be.equal( y )`~~.
 * It should be `expect( x ).to.equal( y )`. **NOT**: ~~`expect( x ).to.be.equal( y )`~~.
+* When using Sinon spies, pay attention to the readability of assertions and failure messages.
+   * Use named spies, for example:
+
+		```js
+		const someCallbackSpy = sinon.spy().named( 'someCallback' );
+		const myMethodSpy = sinon.spy( obj, 'myMethod' );
+		```
+   * Use [sinon-chai assertions](https://www.chaijs.com/plugins/sinon-chai/)
+
+		```js
+		expect( myMethodSpy ).to.be.calledOnce 
+		// expected myMethod to be called once but was called twice
+		```
 
 
 ## Naming
 ## Naming
 
 

+ 79 - 0
packages/ckeditor5-core/tests/_utils-tests/assertions/attribute.js

@@ -0,0 +1,79 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* global chai */
+
+import '../../_utils/assertions/attribute';
+
+describe( 'attribute chai assertion', () => {
+	it( 'should be added to chai assertions', () => {
+		const assertion = new chai.Assertion();
+
+		expect( assertion ).to.have.property( 'attribute' );
+		expect( assertion.attribute ).to.be.instanceof( Function );
+	} );
+
+	it( 'should assert the target has a \'hasAttribute\' method', () => {
+		expect( { hasAttribute: () => true } ).to.have.attribute( 'foo' );
+
+		expect( function() {
+			expect( {} ).not.to.have.attribute( 'bar' );
+		} ).to.throw( 'expected {} to respond to \'hasAttribute\'' );
+
+		expect( function() {
+			expect( {} ).to.have.attribute( 'bar' );
+		} ).to.throw( 'expected {} to respond to \'hasAttribute\'' );
+	} );
+
+	it( 'should assert the \'target.hasAttribute\' returns \'true\' for the given type', () => {
+		expect( { hasAttribute: () => true } ).to.have.attribute( 'foo' );
+
+		expect( function() {
+			expect( { hasAttribute: () => false } ).to.have.attribute( 'bar' );
+		} ).to.throw( 'expected { Object (hasAttribute) } to have attribute \'bar\'' );
+	} );
+
+	it( 'negated, should assert the \'target.hasAttribute\' returns \'false\' for the given type', () => {
+		expect( { hasAttribute: () => false } ).not.to.have.attribute( 'foo' );
+
+		expect( function() {
+			expect( { hasAttribute: () => true } ).not.to.have.attribute( 'bar' );
+		} ).to.throw( 'expected { Object (hasAttribute) } to not have attribute \'bar\'' );
+	} );
+
+	it( 'should assert the \'target.getAttribute\' returns the given value for the given type', () => {
+		expect( {
+			hasAttribute: () => true,
+			getAttribute: () => 'bar'
+		} ).to.have.attribute( 'foo', 'bar' );
+
+		expect( function() {
+			expect( {
+				hasAttribute: () => true,
+				getAttribute: () => 'bar'
+			} ).to.have.attribute( 'foo', 'baz' );
+		} ).to.throw( 'expected { Object (hasAttribute, getAttribute) } to have attribute \'foo\' of \'bar\', but got \'baz\'' );
+	} );
+
+	it( 'negated, should assert for the given type the \'target.getAttribute\' returns a value different than the given one', () => {
+		expect( {
+			hasAttribute: () => true,
+			getAttribute: () => 'bar'
+		} ).to.not.have.attribute( 'foo', 'baz' );
+
+		expect( function() {
+			expect( {
+				hasAttribute: () => true,
+				getAttribute: () => 'baz'
+			} ).to.not.have.attribute( 'foo', 'baz' );
+		} ).to.throw( 'expected { Object (hasAttribute, getAttribute) } to not have attribute \'foo\' of \'baz\'' );
+	} );
+
+	it( 'should prefix failure message with the given one', () => {
+		expect( function() {
+			expect( {} ).to.have.attribute( 'foo', 'baz', 'Illegal salmon' );
+		} ).to.throw( /^Illegal salmon: / );
+	} );
+} );

+ 61 - 0
packages/ckeditor5-core/tests/_utils/assertions/attribute.js

@@ -0,0 +1,61 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* global chai */
+
+/**
+ * Asserts that the target has an attribute with the given key name.
+ * See {@link module:engine/model/documentselection~DocumentSelection#hasAttribute hasAttribute}.
+ *
+ *		expect( selection ).to.have.attribute( 'linkHref' );
+ *
+ * When `value` is provided, .attribute also asserts that the attribute's value is equal to the given `value`.
+ * See {@link module:engine/model/documentselection~DocumentSelection#getAttribute getAttribute}.
+ *
+ *		expect( selection ).to.have.attribute( 'linkHref', 'example.com' );
+ *
+ * Negations works as well.
+ *
+ * @param {String} key Key of attribute to assert.
+ * @param {String} [value] Attribute value to assert.
+ * @param {String} [message] Additional message.
+ */
+chai.Assertion.addMethod( 'attribute', function attributeAssertion( key, value, message ) {
+	if ( message ) {
+		chai.util.flag( this, 'message', message );
+	}
+
+	const obj = this._obj;
+
+	if ( arguments.length === 1 ) {
+		// Check if it has the method at all.
+		new chai.Assertion( obj ).to.respondTo( 'hasAttribute' );
+
+		// Check if it has the attribute.
+		const hasAttribute = obj.hasAttribute( key );
+		this.assert(
+			hasAttribute === true,
+			`expected #{this} to have attribute '${ key }'`,
+			`expected #{this} to not have attribute '${ key }'`,
+			!chai.util.flag( this, 'negate' ),
+			hasAttribute
+		);
+	}
+
+	// If a value was given.
+	if ( arguments.length >= 2 ) {
+		// Check if it has the method at all.
+		new chai.Assertion( obj ).to.respondTo( 'getAttribute', message );
+
+		const attributeValue = obj.getAttribute( key );
+		this.assert(
+			attributeValue === value,
+			`expected #{this} to have attribute '${ key }' of #{exp}, but got #{act}`,
+			`expected #{this} to not have attribute '${ key }' of #{exp}`,
+			attributeValue,
+			value
+		);
+	}
+} );

+ 3 - 0
packages/ckeditor5-engine/src/view/downcastwriter.js

@@ -214,6 +214,9 @@ export default class DowncastWriter {
 	 *		writer.createEditableElement( 'div' );
 	 *		writer.createEditableElement( 'div' );
 	 *		writer.createEditableElement( 'div', { id: 'foo-1234' } );
 	 *		writer.createEditableElement( 'div', { id: 'foo-1234' } );
 	 *
 	 *
+	 * Note: The editable element is to be used in the editing pipeline. Usually, together with
+	 * {@link module:widget/utils~toWidgetEditable `toWidgetEditable()`}.
+	 *
 	 * @param {String} name Name of the element.
 	 * @param {String} name Name of the element.
 	 * @param {Object} [attributes] Elements attributes.
 	 * @param {Object} [attributes] Elements attributes.
 	 * @returns {module:engine/view/editableelement~EditableElement} Created element.
 	 * @returns {module:engine/view/editableelement~EditableElement} Created element.

+ 6 - 2
packages/ckeditor5-image/docs/features/image.md

@@ -259,7 +259,11 @@ ClassicEditor
 
 
 ## Linking images
 ## Linking images
 
 
-The {@link module:link/linkimage~LinkImage} plugin adds support for linking images:
+The {@link module:link/linkimage~LinkImage} plugin adds support for linking images. Some use cases where this is needed are:
+
+* Linking to a high-resolution version of an image.
+* Using images as thumbnails linking to an article or product page.
+* Creating banners linking to other pages.
 
 
 ```html
 ```html
 <figure class="image">
 <figure class="image">
@@ -284,7 +288,7 @@ The image linking feature is not enabled by default in any of the editor builds.
 
 
 To add image features to your rich-text editor, install the [`@ckeditor/ckeditor5-image`](https://www.npmjs.com/package/@ckeditor/ckeditor5-image) package:
 To add image features to your rich-text editor, install the [`@ckeditor/ckeditor5-image`](https://www.npmjs.com/package/@ckeditor/ckeditor5-image) package:
 
 
-```bash
+```plaintext
 npm install --save @ckeditor/ckeditor5-image @ckeditor/ckeditor5-link
 npm install --save @ckeditor/ckeditor5-image @ckeditor/ckeditor5-link
 ```
 ```
 
 

+ 6 - 2
packages/ckeditor5-link/docs/api/link.md

@@ -8,13 +8,17 @@ category: api-reference
 
 
 This package implements the link feature for CKEditor 5. It allows for inserting hyperlinks into the edited content and offers the UI to create and edit them.
 This package implements the link feature for CKEditor 5. It allows for inserting hyperlinks into the edited content and offers the UI to create and edit them.
 
 
+## Demo
+
+Check out the demo in the {@link features/link Link feature} guide.
+
 ## Documentation
 ## Documentation
 
 
-See the {@link module:link/link~Link} plugin documentation.
+See the {@link features/link Link feature} guide and the {@link module:link/link~Link} plugin documentation.
 
 
 ## Installation
 ## Installation
 
 
-```bash
+```plaintext
 npm install --save @ckeditor/ckeditor5-link
 npm install --save @ckeditor/ckeditor5-link
 ```
 ```
 
 

+ 7 - 5
packages/ckeditor5-link/docs/features/link.md

@@ -147,11 +147,13 @@ ClassicEditor
 	.catch( ... );
 	.catch( ... );
 ```
 ```
 
 
-#### Adding default link protocol for the external links
+#### Adding default link protocol to external links
 
 
-Default link protocol can be usefull when user forget to type a full URL address to an external source, site etc. Sometimes copying the text, like for example `ckeditor.com` and converting it to a link may cause some issues. When you do this, the created link will direct you to `yourdomain.com/ckeditor.com`, because you forgot to pass the right protocol which makes the link relative to the site where it appears.
+A default link protocol can be useful when the user forgets to type the full URL address to an external source or website. Sometimes copying the text, like for example `ckeditor.com`, and converting it to a link may cause some issues. As a result, the created link will direct you to `yourdomain.com/ckeditor.com` because of the missing protocol. This makes the link relative to the site where it appears.
 
 
-Enabling the `{@link module:link/link~LinkConfig#defaultProtocol config.link.defaultProtocol}`, the {@link module:link/link~Link} feature will handle this issue for you. By default it doesn't fix the passed link value, but when you set `{@link module:link/link~LinkConfig#defaultProtocol config.link.defaultProtocol}` to — for example — `http://`, the plugin will add the given protocol to the every link that may need it (like `ckeditor.com`, `example.com` etc. where `[protocol://]example.com` is missing). Here's the basic configuration example:
+After you enable the {@link module:link/link~LinkConfig#defaultProtocol `config.link.defaultProtocol`} configuration option, the link feature will be able to handle this issue for you. By default it does not fix the passed link value, but when you set {@link module:link/link~LinkConfig#defaultProtocol `config.link.defaultProtocol`} to, for example, `http://`, the plugin will add the given protocol to every link that may need it (like `ckeditor.com`, `example.com`, etc. where `[protocol://]example.com` is missing).
+
+See a basic configuration example:
 
 
 ```js
 ```js
 ClassicEditor
 ClassicEditor
@@ -166,9 +168,9 @@ ClassicEditor
 ```
 ```
 
 
 <info-box>
 <info-box>
-	Having `config.link.defaultProtocol` enabled you are still able to link things locally using `#` or `/`. Protocol won't be added to those links.
+	With the `config.link.defaultProtocol` option enabled, you are still able to link things locally using `#` or `/`. The protocol will not be added to these links.
 
 
-	Enabled feature also gives you an **email addresses auto-detection** feature. When you submit `hello@example.com`, the plugin will change it automatically to `mailto:hello@example.com`.
+	When enabled, this feature also provides the **email address auto-detection** feature. When you submit `hello@example.com` in your content, the plugin will automatically change it to `mailto:hello@example.com`.
 </info-box>
 </info-box>
 
 
 #### Adding attributes to links based on pre–defined rules (automatic decorators)
 #### Adding attributes to links based on pre–defined rules (automatic decorators)

+ 1 - 1
packages/ckeditor5-link/package.json

@@ -13,6 +13,7 @@
     "@ckeditor/ckeditor5-core": "^20.0.0",
     "@ckeditor/ckeditor5-core": "^20.0.0",
     "@ckeditor/ckeditor5-engine": "^20.0.0",
     "@ckeditor/ckeditor5-engine": "^20.0.0",
     "@ckeditor/ckeditor5-image": "^20.0.0",
     "@ckeditor/ckeditor5-image": "^20.0.0",
+    "@ckeditor/ckeditor5-typing": "^20.0.0",
     "@ckeditor/ckeditor5-ui": "^20.0.0",
     "@ckeditor/ckeditor5-ui": "^20.0.0",
     "@ckeditor/ckeditor5-utils": "^20.0.0",
     "@ckeditor/ckeditor5-utils": "^20.0.0",
     "lodash-es": "^4.17.15"
     "lodash-es": "^4.17.15"
@@ -26,7 +27,6 @@
     "@ckeditor/ckeditor5-enter": "^20.0.0",
     "@ckeditor/ckeditor5-enter": "^20.0.0",
     "@ckeditor/ckeditor5-paragraph": "^20.0.0",
     "@ckeditor/ckeditor5-paragraph": "^20.0.0",
     "@ckeditor/ckeditor5-theme-lark": "^20.0.0",
     "@ckeditor/ckeditor5-theme-lark": "^20.0.0",
-    "@ckeditor/ckeditor5-typing": "^20.0.0",
     "@ckeditor/ckeditor5-undo": "^20.0.0"
     "@ckeditor/ckeditor5-undo": "^20.0.0"
   },
   },
   "engines": {
   "engines": {

+ 4 - 4
packages/ckeditor5-link/src/link.js

@@ -59,10 +59,10 @@ export default class Link extends Plugin {
 
 
 /**
 /**
  * When set, the editor will add the given protocol to the link when the user creates a link without one.
  * When set, the editor will add the given protocol to the link when the user creates a link without one.
- * For example, when the user is creating a link and types `ckeditor.com` in the link form input — during link submission —
- * the editor will automatically add the `http://` protocol, so the link will be as follows: `http://ckeditor.com`.
+ * For example, when the user is creating a link and types `ckeditor.com` in the link form input, during link submission
+ * the editor will automatically add the `http://` protocol, so the link will look as follows: `http://ckeditor.com`.
  *
  *
- * The feature also comes with an email auto-detection. When you submit `hello@example.com`
+ * The feature also provides email address auto-detection. When you submit `hello@example.com`,
  * the plugin will automatically change it to `mailto:hello@example.com`.
  * the plugin will automatically change it to `mailto:hello@example.com`.
  *
  *
  * 		ClassicEditor
  * 		ClassicEditor
@@ -74,7 +74,7 @@ export default class Link extends Plugin {
  *			.then( ... )
  *			.then( ... )
  *			.catch( ... );
  *			.catch( ... );
  *
  *
- * **NOTE:** In case no configuration is provided, the editor won't auto-fix the links.
+ * **NOTE:** If no configuration is provided, the editor will not auto-fix the links.
  *
  *
  * @member {String} module:link/link~LinkConfig#defaultProtocol
  * @member {String} module:link/link~LinkConfig#defaultProtocol
  */
  */

+ 10 - 9
packages/ckeditor5-link/src/linkediting.js

@@ -9,7 +9,7 @@
 
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import MouseObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mouseobserver';
 import MouseObserver from '@ckeditor/ckeditor5-engine/src/view/observer/mouseobserver';
-import bindTwoStepCaretToAttribute from '@ckeditor/ckeditor5-engine/src/utils/bindtwostepcarettoattribute';
+import TwoStepCaretMovement from '@ckeditor/ckeditor5-typing/src/twostepcaretmovement';
 import LinkCommand from './linkcommand';
 import LinkCommand from './linkcommand';
 import UnlinkCommand from './unlinkcommand';
 import UnlinkCommand from './unlinkcommand';
 import AutomaticDecorators from './utils/automaticdecorators';
 import AutomaticDecorators from './utils/automaticdecorators';
@@ -40,6 +40,13 @@ export default class LinkEditing extends Plugin {
 		return 'LinkEditing';
 		return 'LinkEditing';
 	}
 	}
 
 
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ TwoStepCaretMovement ];
+	}
+
 	/**
 	/**
 	 * @inheritDoc
 	 * @inheritDoc
 	 */
 	 */
@@ -56,7 +63,6 @@ export default class LinkEditing extends Plugin {
 	 */
 	 */
 	init() {
 	init() {
 		const editor = this.editor;
 		const editor = this.editor;
-		const locale = editor.locale;
 
 
 		// Allow link attribute on all inline nodes.
 		// Allow link attribute on all inline nodes.
 		editor.model.schema.extend( '$text', { allowAttributes: 'linkHref' } );
 		editor.model.schema.extend( '$text', { allowAttributes: 'linkHref' } );
@@ -93,13 +99,8 @@ export default class LinkEditing extends Plugin {
 		this._enableManualDecorators( linkDecorators.filter( item => item.mode === DECORATOR_MANUAL ) );
 		this._enableManualDecorators( linkDecorators.filter( item => item.mode === DECORATOR_MANUAL ) );
 
 
 		// Enable two-step caret movement for `linkHref` attribute.
 		// Enable two-step caret movement for `linkHref` attribute.
-		bindTwoStepCaretToAttribute( {
-			view: editor.editing.view,
-			model: editor.model,
-			emitter: this,
-			attribute: 'linkHref',
-			locale
-		} );
+		const twoStepCaretMovementPlugin = editor.plugins.get( TwoStepCaretMovement );
+		twoStepCaretMovementPlugin.registerAttribute( 'linkHref' );
 
 
 		// Setup highlight over selected link.
 		// Setup highlight over selected link.
 		this._setupLinkHighlight();
 		this._setupLinkHighlight();

+ 3 - 3
packages/ckeditor5-link/src/linkimageui.js

@@ -20,8 +20,8 @@ import linkIcon from '../theme/icons/link.svg';
 /**
 /**
  * The link image UI plugin.
  * The link image UI plugin.
  *
  *
- * This plugin brings a `'linkImage'` button that can be displayed in the {@link module:image/imagetoolbar~ImageToolbar}
- * and used to wrap images in links.
+ * This plugin provides the `'linkImage'` button that can be displayed in the {@link module:image/imagetoolbar~ImageToolbar}.
+ * It can be used to wrap images in links.
  *
  *
  * @extends module:core/plugin~Plugin
  * @extends module:core/plugin~Plugin
  */
  */
@@ -63,7 +63,7 @@ export default class LinkImageUI extends Plugin {
 	 *
 	 *
 	 * Clicking this button shows a {@link module:link/linkui~LinkUI#_balloon} attached to the selection.
 	 * Clicking this button shows a {@link module:link/linkui~LinkUI#_balloon} attached to the selection.
 	 * When an image is already linked, the view shows {@link module:link/linkui~LinkUI#actionsView} or
 	 * When an image is already linked, the view shows {@link module:link/linkui~LinkUI#actionsView} or
-	 * {@link module:link/linkui~LinkUI#formView} if it's not.
+	 * {@link module:link/linkui~LinkUI#formView} if it is not.
 	 *
 	 *
 	 * @private
 	 * @private
 	 */
 	 */

+ 18 - 9
packages/ckeditor5-link/tests/linkediting.js

@@ -75,15 +75,17 @@ describe( 'LinkEditing', () => {
 		expect( model.schema.checkAttribute( [ '$block' ], 'linkHref' ) ).to.be.false;
 		expect( model.schema.checkAttribute( [ '$block' ], 'linkHref' ) ).to.be.false;
 	} );
 	} );
 
 
-	// Let's check only the minimum to not duplicate `bindTwoStepCaretToAttribute()` tests.
+	// Let's check only the minimum to not duplicate `TwoStepCaretMovement` tests.
 	// Testing minimum is better than testing using spies that might give false positive results.
 	// Testing minimum is better than testing using spies that might give false positive results.
 	describe( 'two-step caret movement', () => {
 	describe( 'two-step caret movement', () => {
-		it( 'should be bound to th `linkHref` attribute (LTR)', () => {
+		it( 'should be bound to the `linkHref` attribute (LTR)', () => {
+			const selection = editor.model.document.selection;
+
 			// Put selection before the link element.
 			// Put selection before the link element.
 			setModelData( editor.model, '<paragraph>foo[]<$text linkHref="url">b</$text>ar</paragraph>' );
 			setModelData( editor.model, '<paragraph>foo[]<$text linkHref="url">b</$text>ar</paragraph>' );
 
 
-			// The selection's gravity is not overridden because selection landed here not because of `keydown`.
-			expect( editor.model.document.selection.isGravityOverridden ).to.false;
+			// The selection's gravity should read attributes from the left.
+			expect( selection.hasAttribute( 'linkHref' ), 'hasAttribute( \'linkHref\' )' ).to.be.false;
 
 
 			// So let's simulate the `keydown` event.
 			// So let's simulate the `keydown` event.
 			editor.editing.view.document.fire( 'keydown', {
 			editor.editing.view.document.fire( 'keydown', {
@@ -92,10 +94,13 @@ describe( 'LinkEditing', () => {
 				domTarget: document.body
 				domTarget: document.body
 			} );
 			} );
 
 
-			expect( editor.model.document.selection.isGravityOverridden ).to.true;
+			expect( getModelData( model ) ).to.equal( '<paragraph>foo<$text linkHref="url">[]b</$text>ar</paragraph>' );
+			// Selection should get the attributes from the right.
+			expect( selection.hasAttribute( 'linkHref' ), 'hasAttribute( \'linkHref\' )' ).to.be.true;
+			expect( selection.getAttribute( 'linkHref' ), 'linkHref attribute' ).to.equal( 'url' );
 		} );
 		} );
 
 
-		it( 'should be bound to th `linkHref` attribute (RTL)', async () => {
+		it( 'should be bound to the `linkHref` attribute (RTL)', async () => {
 			const editor = await ClassicTestEditor.create( element, {
 			const editor = await ClassicTestEditor.create( element, {
 				plugins: [ Paragraph, LinkEditing, Enter ],
 				plugins: [ Paragraph, LinkEditing, Enter ],
 				language: {
 				language: {
@@ -105,12 +110,13 @@ describe( 'LinkEditing', () => {
 
 
 			model = editor.model;
 			model = editor.model;
 			view = editor.editing.view;
 			view = editor.editing.view;
+			const selection = editor.model.document.selection;
 
 
 			// Put selection before the link element.
 			// Put selection before the link element.
 			setModelData( editor.model, '<paragraph>foo[]<$text linkHref="url">b</$text>ar</paragraph>' );
 			setModelData( editor.model, '<paragraph>foo[]<$text linkHref="url">b</$text>ar</paragraph>' );
 
 
-			// The selection's gravity is not overridden because selection landed here not because of `keydown`.
-			expect( editor.model.document.selection.isGravityOverridden ).to.false;
+			// The selection's gravity should read attributes from the left.
+			expect( selection.hasAttribute( 'linkHref' ), 'hasAttribute( \'linkHref\' )' ).to.be.false;
 
 
 			// So let's simulate the `keydown` event.
 			// So let's simulate the `keydown` event.
 			editor.editing.view.document.fire( 'keydown', {
 			editor.editing.view.document.fire( 'keydown', {
@@ -119,7 +125,10 @@ describe( 'LinkEditing', () => {
 				domTarget: document.body
 				domTarget: document.body
 			} );
 			} );
 
 
-			expect( editor.model.document.selection.isGravityOverridden ).to.true;
+			expect( getModelData( model ) ).to.equal( '<paragraph>foo<$text linkHref="url">[]b</$text>ar</paragraph>' );
+			// Selection should get the attributes from the right.
+			expect( selection.hasAttribute( 'linkHref' ), 'hasAttribute( \'linkHref\' )' ).to.be.true;
+			expect( selection.getAttribute( 'linkHref' ), 'linkHref attribute' ).to.equal( 'url' );
 
 
 			await editor.destroy();
 			await editor.destroy();
 		} );
 		} );

+ 1 - 1
packages/ckeditor5-typing/src/input.js

@@ -47,7 +47,7 @@ export default class Input extends Plugin {
 	 *		const input = editor.plugins.get( 'Input' );
 	 *		const input = editor.plugins.get( 'Input' );
 	 *
 	 *
 	 *		editor.model.document.on( 'change:data', ( evt, batch ) => {
 	 *		editor.model.document.on( 'change:data', ( evt, batch ) => {
-	 *			if ( input.isTyping( batch ) ) {
+	 *			if ( input.isInput( batch ) ) {
 	 *				console.log( 'The user typed something...' );
 	 *				console.log( 'The user typed something...' );
 	 *			}
 	 *			}
 	 *		} );
 	 *		} );

+ 101 - 59
packages/ckeditor5-engine/src/utils/bindtwostepcarettoattribute.js → packages/ckeditor5-typing/src/twostepcaretmovement.js

@@ -4,27 +4,30 @@
  */
  */
 
 
 /**
 /**
- * @module engine/utils/bindtwostepcarettoattribute
+ * @module typing/twostepcaretmovement
  */
  */
 
 
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
 import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
 
 
 /**
 /**
- * This helper enables the two-step caret (phantom) movement behavior for the given {@link module:engine/model/model~Model}
- * attribute on arrow right (<kbd>→</kbd>) and left (<kbd>←</kbd>) key press.
+ * This plugin enables the two-step caret (phantom) movement behavior for
+ * {@link module:typing/twostepcaretmovement~TwoStepCaretMovement#registerAttribute registered attributes}
+ * on arrow right (<kbd>→</kbd>) and left (<kbd>←</kbd>) key press.
  *
  *
  * Thanks to this (phantom) caret movement the user is able to type before/after as well as at the
  * Thanks to this (phantom) caret movement the user is able to type before/after as well as at the
  * beginning/end of an attribute.
  * beginning/end of an attribute.
  *
  *
- * **Note:** This helper support right–to–left (Arabic, Hebrew, etc.) content by mirroring its behavior
+ * **Note:** This plugin support right–to–left (Arabic, Hebrew, etc.) content by mirroring its behavior
  * but for the sake of simplicity examples showcase only left–to–right use–cases.
  * but for the sake of simplicity examples showcase only left–to–right use–cases.
  *
  *
  * # Forward movement
  * # Forward movement
  *
  *
  * ## "Entering" an attribute:
  * ## "Entering" an attribute:
  *
  *
- * When this behavior is enabled for the `a` attribute and the selection is right before it
+ * When this plugin is enabled and registered for the `a` attribute and the selection is right before it
  * (at the attribute boundary), pressing the right arrow key will not move the selection but update its
  * (at the attribute boundary), pressing the right arrow key will not move the selection but update its
  * attributes accordingly:
  * attributes accordingly:
  *
  *
@@ -80,70 +83,109 @@ import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
  *   <kbd>←</kbd>
  *   <kbd>←</kbd>
  *
  *
  *   		<$text a="true">ba{}r</$text>b{}az
  *   		<$text a="true">ba{}r</$text>b{}az
- *
- * @param {Object} options Helper options.
- * @param {module:engine/view/view~View} options.view View controller instance.
- * @param {module:engine/model/model~Model} options.model Data model instance.
- * @param {module:utils/dom/emittermixin~Emitter} options.emitter The emitter to which this behavior should be added
- * (e.g. a plugin instance).
- * @param {String} options.attribute Attribute for which this behavior will be added.
- * @param {module:utils/locale~Locale} options.locale The {@link module:core/editor/editor~Editor#locale} instance.
  */
  */
-export default function bindTwoStepCaretToAttribute( { view, model, emitter, attribute, locale } ) {
-	const twoStepCaretHandler = new TwoStepCaretHandler( model, emitter, attribute );
-	const modelSelection = model.document.selection;
-
-	// Listen to keyboard events and handle the caret movement according to the 2-step caret logic.
-	//
-	// Note: This listener has the "high+1" priority:
-	// * "high" because of the filler logic implemented in the renderer which also engages on #keydown.
-	// When the gravity is overridden the attributes of the (model) selection attributes are reset.
-	// It may end up with the filler kicking in and breaking the selection.
-	// * "+1" because we would like to avoid collisions with other features (like Widgets), which
-	// take over the keydown events with the "high" priority. Two-step caret movement takes precedence
-	// over Widgets in that matter.
-	//
-	// Find out more in https://github.com/ckeditor/ckeditor5-engine/issues/1301.
-	emitter.listenTo( view.document, 'keydown', ( evt, data ) => {
-		// This implementation works only for collapsed selection.
-		if ( !modelSelection.isCollapsed ) {
-			return;
-		}
+export default class TwoStepCaretMovement extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'TwoStepCaretMovement';
+	}
 
 
-		// When user tries to expand the selection or jump over the whole word or to the beginning/end then
-		// two-steps movement is not necessary.
-		if ( data.shiftKey || data.altKey || data.ctrlKey ) {
-			return;
-		}
+	/**
+	 * @inheritDoc
+	 */
+	constructor( editor ) {
+		super( editor );
 
 
-		const arrowRightPressed = data.keyCode == keyCodes.arrowright;
-		const arrowLeftPressed = data.keyCode == keyCodes.arrowleft;
+		/**
+		 * A map of handlers for each attribute.
+		 *
+		 * @protected
+		 * @property {module:typing/twostepcaretmovement~TwoStepCaretMovement}
+		 */
+		this._handlers = new Map();
+	}
 
 
-		// When neither left or right arrow has been pressed then do noting.
-		if ( !arrowRightPressed && !arrowLeftPressed ) {
-			return;
-		}
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const model = editor.model;
+		const view = editor.editing.view;
+		const locale = editor.locale;
 
 
-		const position = modelSelection.getFirstPosition();
-		const contentDirection = locale.contentLanguageDirection;
-		let isMovementHandled;
+		const modelSelection = model.document.selection;
 
 
-		if ( ( contentDirection === 'ltr' && arrowRightPressed ) || ( contentDirection === 'rtl' && arrowLeftPressed ) ) {
-			isMovementHandled = twoStepCaretHandler.handleForwardMovement( position, data );
-		} else {
-			isMovementHandled = twoStepCaretHandler.handleBackwardMovement( position, data );
-		}
+		// Listen to keyboard events and handle the caret movement according to the 2-step caret logic.
+		//
+		// Note: This listener has the "high+1" priority:
+		// * "high" because of the filler logic implemented in the renderer which also engages on #keydown.
+		// When the gravity is overridden the attributes of the (model) selection attributes are reset.
+		// It may end up with the filler kicking in and breaking the selection.
+		// * "+1" because we would like to avoid collisions with other features (like Widgets), which
+		// take over the keydown events with the "high" priority. Two-step caret movement takes precedence
+		// over Widgets in that matter.
+		//
+		// Find out more in https://github.com/ckeditor/ckeditor5-engine/issues/1301.
+		this.listenTo( view.document, 'keydown', ( evt, data ) => {
+			// This implementation works only for collapsed selection.
+			if ( !modelSelection.isCollapsed ) {
+				return;
+			}
 
 
-		// Stop the keydown event if the two-step caret movement handled it. Avoid collisions
-		// with other features which may also take over the caret movement (e.g. Widget).
-		if ( isMovementHandled ) {
-			evt.stop();
-		}
-	}, { priority: priorities.get( 'high' ) + 1 } );
+			// When user tries to expand the selection or jump over the whole word or to the beginning/end then
+			// two-steps movement is not necessary.
+			if ( data.shiftKey || data.altKey || data.ctrlKey ) {
+				return;
+			}
+
+			const arrowRightPressed = data.keyCode == keyCodes.arrowright;
+			const arrowLeftPressed = data.keyCode == keyCodes.arrowleft;
+
+			// When neither left or right arrow has been pressed then do noting.
+			if ( !arrowRightPressed && !arrowLeftPressed ) {
+				return;
+			}
+
+			const position = modelSelection.getFirstPosition();
+			const contentDirection = locale.contentLanguageDirection;
+			let isMovementHandled = false;
+
+			if ( ( contentDirection === 'ltr' && arrowRightPressed ) || ( contentDirection === 'rtl' && arrowLeftPressed ) ) {
+				for ( const [ , handler ] of this._handlers ) {
+					isMovementHandled = isMovementHandled || handler.handleForwardMovement( position, data );
+				}
+			} else {
+				for ( const [ , handler ] of this._handlers ) {
+					isMovementHandled = isMovementHandled || handler.handleBackwardMovement( position, data );
+				}
+			}
+
+			// Stop the keydown event if the two-step caret movement handled it. Avoid collisions
+			// with other features which may also take over the caret movement (e.g. Widget).
+			if ( isMovementHandled ) {
+				evt.stop();
+			}
+		}, { priority: priorities.get( 'high' ) + 1 } );
+	}
+
+	/**
+	 * Registers a given attribute for the two-step caret movement.
+	 *
+	 * @param {String} attribute Name of the attribute to handle.
+	 */
+	registerAttribute( attribute ) {
+		this._handlers.set(
+			attribute,
+			new TwoStepCaretHandler( this.editor.model, this, attribute )
+		);
+	}
 }
 }
 
 
 /**
 /**
- * This is a protected helper–class for {@link module:engine/utils/bindtwostepcarettoattribute}.
+ * This is a protected helper–class for {@link module:typing/twostepcaretmovement}.
  * It handles the state of the 2-step caret movement for a single {@link module:engine/model/model~Model}
  * It handles the state of the 2-step caret movement for a single {@link module:engine/model/model~Model}
  * attribute upon the `keypress` in the {@link module:engine/view/view~View}.
  * attribute upon the `keypress` in the {@link module:engine/view/view~View}.
  *
  *

+ 0 - 0
packages/ckeditor5-engine/tests/manual/tickets/1301/1.html → packages/ckeditor5-typing/tests/manual/1301/1.html


+ 4 - 10
packages/ckeditor5-engine/tests/manual/tickets/1301/1.js → packages/ckeditor5-typing/tests/manual/1301/1.js

@@ -11,23 +11,17 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
 import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
 import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
 import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
 
 
-import bindTwoStepCaretToAttribute from '../../../../src/utils/bindtwostepcarettoattribute';
+import TwoStepCaretMovement from '../../../src/twostepcaretmovement';
 
 
 ClassicEditor
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
 	.create( document.querySelector( '#editor' ), {
-		plugins: [ Essentials, Paragraph, Bold, Italic ],
+		plugins: [ Essentials, Paragraph, Bold, Italic, TwoStepCaretMovement ],
 		toolbar: [ 'undo', 'redo', '|', 'bold', 'italic' ]
 		toolbar: [ 'undo', 'redo', '|', 'bold', 'italic' ]
 	} )
 	} )
 	.then( editor => {
 	.then( editor => {
-		const bold = editor.plugins.get( Bold );
+		const twoStepCaretMovement = editor.plugins.get( TwoStepCaretMovement );
 
 
-		bindTwoStepCaretToAttribute( {
-			view: editor.editing.view,
-			model: editor.model,
-			emitter: bold,
-			attribute: 'bold',
-			locale: editor.locale
-		} );
+		twoStepCaretMovement.registerAttribute( 'bold' );
 	} )
 	} )
 	.catch( err => {
 	.catch( err => {
 		console.error( err.stack );
 		console.error( err.stack );

+ 1 - 1
packages/ckeditor5-engine/tests/manual/tickets/1301/1.md → packages/ckeditor5-typing/tests/manual/1301/1.md

@@ -1,4 +1,4 @@
-## Two-steps caret movement [#1301](https://github.com/ckeditor/ckeditor5-engine/issues/1301)
+## Two-steps caret movement [ckeditor5-engine#1301](https://github.com/ckeditor/ckeditor5-engine/issues/1301)
 
 
 1. Put the selection at the end of the content.
 1. Put the selection at the end of the content.
 2. Press the <kbd>←</kbd> key 3 times.
 2. Press the <kbd>←</kbd> key 3 times.

+ 0 - 0
packages/ckeditor5-engine/tests/manual/two-step-caret.html → packages/ckeditor5-typing/tests/manual/two-step-caret.html


+ 9 - 35
packages/ckeditor5-engine/tests/manual/two-step-caret.js → packages/ckeditor5-typing/tests/manual/two-step-caret.js

@@ -12,31 +12,18 @@ import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline';
 import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
 import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
 import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
 import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
 
 
-import bindTwoStepCaretToAttribute from '../../src/utils/bindtwostepcarettoattribute';
+import TwoStepCaretMovement from '../../src/twostepcaretmovement';
 
 
 ClassicEditor
 ClassicEditor
 	.create( document.querySelector( '#editor-ltr' ), {
 	.create( document.querySelector( '#editor-ltr' ), {
-		plugins: [ Essentials, Paragraph, Underline, Bold, Italic ],
+		plugins: [ Essentials, Paragraph, Underline, Bold, Italic, TwoStepCaretMovement ],
 		toolbar: [ 'undo', 'redo', '|', 'bold', 'underline', 'italic' ]
 		toolbar: [ 'undo', 'redo', '|', 'bold', 'underline', 'italic' ]
 	} )
 	} )
 	.then( editor => {
 	.then( editor => {
-		const bold = editor.plugins.get( Italic );
-		const underline = editor.plugins.get( Underline );
+		const twoStepCaretMovement = editor.plugins.get( TwoStepCaretMovement );
 
 
-		bindTwoStepCaretToAttribute( {
-			view: editor.editing.view,
-			model: editor.model,
-			emitter: bold,
-			attribute: 'italic',
-			locale: editor.locale
-		} );
-		bindTwoStepCaretToAttribute( {
-			view: editor.editing.view,
-			model: editor.model,
-			emitter: underline,
-			attribute: 'underline',
-			locale: editor.locale
-		} );
+		twoStepCaretMovement.registerAttribute( 'italic' );
+		twoStepCaretMovement.registerAttribute( 'underline' );
 	} )
 	} )
 	.catch( err => {
 	.catch( err => {
 		console.error( err.stack );
 		console.error( err.stack );
@@ -47,27 +34,14 @@ ClassicEditor
 		language: {
 		language: {
 			content: 'he'
 			content: 'he'
 		},
 		},
-		plugins: [ Essentials, Paragraph, Underline, Bold, Italic ],
+		plugins: [ Essentials, Paragraph, Underline, Bold, Italic, TwoStepCaretMovement ],
 		toolbar: [ 'undo', 'redo', '|', 'bold', 'underline', 'italic' ]
 		toolbar: [ 'undo', 'redo', '|', 'bold', 'underline', 'italic' ]
 	} )
 	} )
 	.then( editor => {
 	.then( editor => {
-		const bold = editor.plugins.get( Italic );
-		const underline = editor.plugins.get( Underline );
+		const twoStepCaretMovement = editor.plugins.get( TwoStepCaretMovement );
 
 
-		bindTwoStepCaretToAttribute( {
-			view: editor.editing.view,
-			model: editor.model,
-			emitter: bold,
-			attribute: 'italic',
-			locale: editor.locale
-		} );
-		bindTwoStepCaretToAttribute( {
-			view: editor.editing.view,
-			model: editor.model,
-			emitter: underline,
-			attribute: 'underline',
-			locale: editor.locale
-		} );
+		twoStepCaretMovement.registerAttribute( 'italic' );
+		twoStepCaretMovement.registerAttribute( 'underline' );
 	} )
 	} )
 	.catch( err => {
 	.catch( err => {
 		console.error( err.stack );
 		console.error( err.stack );

+ 1 - 1
packages/ckeditor5-engine/tests/manual/two-step-caret.md → packages/ckeditor5-typing/tests/manual/two-step-caret.md

@@ -1,4 +1,4 @@
-## Two-steps caret movement [#1286](https://github.com/ckeditor/ckeditor5-engine/issues/1289)
+## Two-steps caret movement [ckeditor5-engine#1286](https://github.com/ckeditor/ckeditor5-engine/issues/1289)
 
 
 ### Moving right
 ### Moving right
 1. Put selection one character before the underline
 1. Put selection one character before the underline

+ 12 - 30
packages/ckeditor5-engine/tests/utils/bindtwostepcarettoattribute.js → packages/ckeditor5-typing/tests/twostepcaretmovement.js

@@ -7,16 +7,16 @@
 
 
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin';
 import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin';
-import DomEventData from '../../src/view/observer/domeventdata';
+import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
 import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo';
 import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo';
-import bindTwoStepCaretToAttribute, { TwoStepCaretHandler } from '../../src/utils/bindtwostepcarettoattribute';
-import Position from '../../src/model/position';
+import TwoStepCaretMovement, { TwoStepCaretHandler } from '../src/twostepcaretmovement';
+import Position from '@ckeditor/ckeditor5-engine/src/model/position';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
-import { setData } from '../../src/dev-utils/model';
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 
-describe( 'bindTwoStepCaretToAttribute()', () => {
-	let editor, model, emitter, selection, view, locale;
+describe( 'TwoStepCaretMovement()', () => {
+	let editor, model, emitter, selection, view, plugin;
 	let preventDefaultSpy, evtStopSpy;
 	let preventDefaultSpy, evtStopSpy;
 
 
 	testUtils.createSinonSandbox();
 	testUtils.createSinonSandbox();
@@ -24,12 +24,12 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 	beforeEach( () => {
 	beforeEach( () => {
 		emitter = Object.create( DomEmitterMixin );
 		emitter = Object.create( DomEmitterMixin );
 
 
-		return VirtualTestEditor.create().then( newEditor => {
+		return VirtualTestEditor.create( { plugins: [ TwoStepCaretMovement ] } ).then( newEditor => {
 			editor = newEditor;
 			editor = newEditor;
 			model = editor.model;
 			model = editor.model;
 			selection = model.document.selection;
 			selection = model.document.selection;
 			view = editor.editing.view;
 			view = editor.editing.view;
-			locale = editor.locale;
+			plugin = editor.plugins.get( TwoStepCaretMovement );
 
 
 			preventDefaultSpy = sinon.spy();
 			preventDefaultSpy = sinon.spy();
 			evtStopSpy = sinon.spy();
 			evtStopSpy = sinon.spy();
@@ -45,13 +45,7 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 			editor.conversion.for( 'upcast' ).elementToAttribute( { view: 'c', model: 'c' } );
 			editor.conversion.for( 'upcast' ).elementToAttribute( { view: 'c', model: 'c' } );
 			editor.conversion.elementToElement( { model: 'paragraph', view: 'p' } );
 			editor.conversion.elementToElement( { model: 'paragraph', view: 'p' } );
 
 
-			bindTwoStepCaretToAttribute( {
-				view: editor.editing.view,
-				model: editor.model,
-				emitter,
-				attribute: 'a',
-				locale
-			} );
+			plugin.registerAttribute( 'a' );
 		} );
 		} );
 	} );
 	} );
 
 
@@ -560,13 +554,7 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 
 
 	describe( 'multiple attributes', () => {
 	describe( 'multiple attributes', () => {
 		beforeEach( () => {
 		beforeEach( () => {
-			bindTwoStepCaretToAttribute( {
-				view: editor.editing.view,
-				model: editor.model,
-				emitter,
-				attribute: 'c',
-				locale
-			} );
+			plugin.registerAttribute( 'c' );
 		} );
 		} );
 
 
 		it( 'should work with the two-step caret movement (moving right)', () => {
 		it( 'should work with the two-step caret movement (moving right)', () => {
@@ -786,12 +774,12 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 		it( 'should use the opposite helper methods (RTL content direction)', () => {
 		it( 'should use the opposite helper methods (RTL content direction)', () => {
 			const forwardStub = testUtils.sinon.stub( TwoStepCaretHandler.prototype, 'handleForwardMovement' );
 			const forwardStub = testUtils.sinon.stub( TwoStepCaretHandler.prototype, 'handleForwardMovement' );
 			const backwardStub = testUtils.sinon.stub( TwoStepCaretHandler.prototype, 'handleBackwardMovement' );
 			const backwardStub = testUtils.sinon.stub( TwoStepCaretHandler.prototype, 'handleBackwardMovement' );
-			const emitter = Object.create( DomEmitterMixin );
 
 
 			let model;
 			let model;
 
 
 			return VirtualTestEditor
 			return VirtualTestEditor
 				.create( {
 				.create( {
+					plugins: [ TwoStepCaretMovement ],
 					language: {
 					language: {
 						content: 'ar'
 						content: 'ar'
 					}
 					}
@@ -812,13 +800,7 @@ describe( 'bindTwoStepCaretToAttribute()', () => {
 					newEditor.conversion.for( 'upcast' ).elementToAttribute( { view: 'c', model: 'c' } );
 					newEditor.conversion.for( 'upcast' ).elementToAttribute( { view: 'c', model: 'c' } );
 					newEditor.conversion.elementToElement( { model: 'paragraph', view: 'p' } );
 					newEditor.conversion.elementToElement( { model: 'paragraph', view: 'p' } );
 
 
-					bindTwoStepCaretToAttribute( {
-						view: newEditor.editing.view,
-						model: newEditor.model,
-						emitter,
-						attribute: 'a',
-						locale: newEditor.locale
-					} );
+					newEditor.plugins.get( TwoStepCaretMovement ).registerAttribute( 'a' );
 
 
 					return newEditor;
 					return newEditor;
 				} )
 				} )

+ 13 - 2
scripts/bump-year.js

@@ -28,18 +28,29 @@ const includeDotFiles = {
 	dot: true
 	dot: true
 };
 };
 
 
-glob( '!(build|coverage|node_modules|packages)/**', updateYear );
+glob( '!(build|coverage|node_modules|external)/**', updateYear );
 
 
 // LICENSE.md, .eslintrc.js, etc.
 // LICENSE.md, .eslintrc.js, etc.
 glob( '*', includeDotFiles, updateYear );
 glob( '*', includeDotFiles, updateYear );
 
 
 function updateYear( err, fileNames ) {
 function updateYear( err, fileNames ) {
 	const filteredFileNames = fileNames.filter( fileName => {
 	const filteredFileNames = fileNames.filter( fileName => {
-		// Filter out stuff from ckeditor5-utils/src/lib.
+		// Filter out nested `node_modules`.
+		if ( minimatch( fileName, '**/node_modules/**' ) ) {
+			return false;
+		}
+
+		// Filter out stuff from `src/lib/`.
 		if ( minimatch( fileName, '**/src/lib/**' ) ) {
 		if ( minimatch( fileName, '**/src/lib/**' ) ) {
 			return false;
 			return false;
 		}
 		}
 
 
+		// Filter out builds.
+		if ( minimatch( fileName, '**/ckeditor5-build-*/build/**' ) ) {
+			return false;
+		}
+
+		// Filter out directories.
 		if ( fs.statSync( fileName ).isDirectory() ) {
 		if ( fs.statSync( fileName ).isDirectory() ) {
 			return false;
 			return false;
 		}
 		}