Browse Source

Merge branch 'master' into t/1667

Mateusz Samsel 6 years ago
parent
commit
04e9075f47

+ 5 - 0
packages/ckeditor5-engine/CHANGELOG.md

@@ -1,6 +1,11 @@
 Changelog
 =========
 
+## [13.2.1](https://github.com/ckeditor/ckeditor5-engine/compare/v13.2.0...v13.2.1) (2019-07-10)
+
+Internal changes only (updated dependencies, documentation, etc.).
+
+
 ## [13.2.0](https://github.com/ckeditor/ckeditor5-engine/compare/v13.1.1...v13.2.0) (2019-07-04)
 
 ### Features

+ 17 - 17
packages/ckeditor5-engine/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@ckeditor/ckeditor5-engine",
-  "version": "13.2.0",
+  "version": "13.2.1",
   "description": "The editing engine of CKEditor 5 – the best browser-based rich text editor.",
   "keywords": [
     "wysiwyg",
@@ -21,26 +21,26 @@
     "ckeditor 5"
   ],
   "dependencies": {
-    "@ckeditor/ckeditor5-utils": "^13.0.0",
+    "@ckeditor/ckeditor5-utils": "^13.0.1",
     "lodash-es": "^4.17.10"
   },
   "devDependencies": {
-    "@ckeditor/ckeditor5-basic-styles": "^11.1.2",
-    "@ckeditor/ckeditor5-block-quote": "^11.1.1",
-    "@ckeditor/ckeditor5-core": "^12.2.0",
-    "@ckeditor/ckeditor5-editor-classic": "^12.1.2",
-    "@ckeditor/ckeditor5-enter": "^11.0.3",
-    "@ckeditor/ckeditor5-essentials": "^11.0.3",
-    "@ckeditor/ckeditor5-heading": "^11.0.3",
-    "@ckeditor/ckeditor5-link": "^11.1.0",
-    "@ckeditor/ckeditor5-list": "^12.0.3",
-    "@ckeditor/ckeditor5-paragraph": "^11.0.3",
-    "@ckeditor/ckeditor5-theme-lark": "^14.1.0",
-    "@ckeditor/ckeditor5-typing": "^12.1.0",
-    "@ckeditor/ckeditor5-undo": "^11.0.3",
-    "@ckeditor/ckeditor5-widget": "^11.0.3",
+    "@ckeditor/ckeditor5-basic-styles": "^11.1.3",
+    "@ckeditor/ckeditor5-block-quote": "^11.1.2",
+    "@ckeditor/ckeditor5-core": "^12.2.1",
+    "@ckeditor/ckeditor5-editor-classic": "^12.1.3",
+    "@ckeditor/ckeditor5-enter": "^11.0.4",
+    "@ckeditor/ckeditor5-essentials": "^11.0.4",
+    "@ckeditor/ckeditor5-heading": "^11.0.4",
+    "@ckeditor/ckeditor5-link": "^11.1.1",
+    "@ckeditor/ckeditor5-list": "^12.0.4",
+    "@ckeditor/ckeditor5-paragraph": "^11.0.4",
+    "@ckeditor/ckeditor5-theme-lark": "^14.1.1",
+    "@ckeditor/ckeditor5-typing": "^12.1.1",
+    "@ckeditor/ckeditor5-undo": "^11.0.4",
+    "@ckeditor/ckeditor5-widget": "^11.0.4",
     "eslint": "^5.5.0",
-    "eslint-config-ckeditor5": "^1.0.11",
+    "eslint-config-ckeditor5": "^2.0.0",
     "husky": "^1.3.1",
     "lint-staged": "^7.0.0"
   },

+ 15 - 15
packages/ckeditor5-engine/src/conversion/downcasthelpers.js

@@ -3,6 +3,12 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
+/**
+ * Contains downcast (model-to-view) converters for {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}.
+ *
+ * @module engine/conversion/downcasthelpers
+ */
+
 import ModelRange from '../model/range';
 import ModelSelection from '../model/selection';
 import ModelElement from '../model/element';
@@ -11,14 +17,8 @@ import ViewAttributeElement from '../view/attributeelement';
 import DocumentSelection from '../model/documentselection';
 import ConversionHelpers from './conversionhelpers';
 
-import log from '@ckeditor/ckeditor5-utils/src/log';
 import { cloneDeep } from 'lodash-es';
-
-/**
- * Contains downcast (model-to-view) converters for {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}.
- *
- * @module engine/conversion/downcasthelpers
- */
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
  * Downcast conversion helper functions.
@@ -730,9 +730,8 @@ function removeUIElement() {
 			return;
 		}
 
-		conversionApi.mapper.unbindElementsFromMarkerName( data.markerName );
-
 		for ( const element of elements ) {
+			conversionApi.mapper.unbindElementFromMarkerName( element, data.markerName );
 			conversionApi.writer.clear( conversionApi.writer.createRangeOn( element ), element );
 		}
 
@@ -824,10 +823,11 @@ function changeAttribute( attributeCreator ) {
 			 *
 			 * @error conversion-attribute-to-attribute-on-text
 			 */
-			log.warn( 'conversion-attribute-to-attribute-on-text: ' +
-				'Trying to convert text node\'s attribute with attribute-to-attribute converter.' );
-
-			return;
+			throw new CKEditorError(
+				'conversion-attribute-to-attribute-on-text: ' +
+				'Trying to convert text node\'s attribute with attribute-to-attribute converter.',
+				[ data, conversionApi ]
+			);
 		}
 
 		// First remove the old attribute if there was one.
@@ -1030,9 +1030,9 @@ function removeHighlight( highlightDescriptor ) {
 			return;
 		}
 
-		conversionApi.mapper.unbindElementsFromMarkerName( data.markerName );
-
 		for ( const element of elements ) {
+			conversionApi.mapper.unbindElementFromMarkerName( element, data.markerName );
+
 			if ( element.is( 'attributeElement' ) ) {
 				conversionApi.writer.unwrap( conversionApi.writer.createRangeOn( element ), viewHighlightElement );
 			} else {

+ 12 - 3
packages/ckeditor5-engine/src/conversion/mapper.js

@@ -174,12 +174,21 @@ export default class Mapper {
 	}
 
 	/**
-	 * Unbinds all elements from given marker name.
+	 * Unbinds an element from given marker name.
 	 *
+	 * @param {module:engine/view/element~Element} element Element to unbind.
 	 * @param {String} name Marker name.
 	 */
-	unbindElementsFromMarkerName( name ) {
-		this._markerNameToElements.delete( name );
+	unbindElementFromMarkerName( element, name ) {
+		const elements = this._markerNameToElements.get( name );
+
+		if ( elements ) {
+			elements.delete( element );
+
+			if ( elements.size == 0 ) {
+				this._markerNameToElements.delete( name );
+			}
+		}
 	}
 
 	/**

+ 39 - 5
packages/ckeditor5-engine/src/model/differ.js

@@ -111,6 +111,32 @@ export default class Differ {
 	}
 
 	/**
+	 * Marks given `item` in differ to be "refreshed". It means that the item will be marked as removed and inserted in the differ changes
+	 * set, so it will be effectively re-converted when differ changes will be handled by a dispatcher.
+	 *
+	 * @param {module:engine/model/item~Item} item Item to refresh.
+	 */
+	refreshItem( item ) {
+		if ( this._isInInsertedElement( item.parent ) ) {
+			return;
+		}
+
+		this._markRemove( item.parent, item.startOffset, item.offsetSize );
+		this._markInsert( item.parent, item.startOffset, item.offsetSize );
+
+		const range = Range._createOn( item );
+
+		for ( const marker of this._markerCollection.getMarkersIntersectingRange( range ) ) {
+			const markerRange = marker.getRange();
+
+			this.bufferMarkerChange( marker.name, markerRange, markerRange, marker.affectsData );
+		}
+
+		// Clear cache after each buffered operation as it is no longer valid.
+		this._cachedChanges = null;
+	}
+
+	/**
 	 * Buffers the given operation. An operation has to be buffered before it is executed.
 	 *
 	 * Operation type is checked and it is checked which nodes it will affect. These nodes are then stored in `Differ`
@@ -136,7 +162,7 @@ export default class Differ {
 			case 'addAttribute':
 			case 'removeAttribute':
 			case 'changeAttribute': {
-				for ( const item of operation.range.getItems() ) {
+				for ( const item of operation.range.getItems( { shallow: true } ) ) {
 					if ( this._isInInsertedElement( item.parent ) ) {
 						continue;
 					}
@@ -1086,19 +1112,25 @@ function _generateActionsFromChanges( oldChildrenLength, changes ) {
 	for ( const change of changes ) {
 		// First, fill "holes" between changes with "equal" actions.
 		if ( change.offset > offset ) {
-			actions.push( ...'e'.repeat( change.offset - offset ).split( '' ) );
+			for ( let i = 0; i < change.offset - offset; i++ ) {
+				actions.push( 'e' );
+			}
 
 			oldChildrenHandled += change.offset - offset;
 		}
 
 		// Then, fill up actions accordingly to change type.
 		if ( change.type == 'insert' ) {
-			actions.push( ...'i'.repeat( change.howMany ).split( '' ) );
+			for ( let i = 0; i < change.howMany; i++ ) {
+				actions.push( 'i' );
+			}
 
 			// The last handled offset is after inserted range.
 			offset = change.offset + change.howMany;
 		} else if ( change.type == 'remove' ) {
-			actions.push( ...'r'.repeat( change.howMany ).split( '' ) );
+			for ( let i = 0; i < change.howMany; i++ ) {
+				actions.push( 'r' );
+			}
 
 			// The last handled offset is at the position where the nodes were removed.
 			offset = change.offset;
@@ -1117,7 +1149,9 @@ function _generateActionsFromChanges( oldChildrenLength, changes ) {
 	// Fill "equal" actions at the end of actions set. Use `oldChildrenHandled` to see how many children
 	// has not been changed / removed at the end of their parent.
 	if ( oldChildrenHandled < oldChildrenLength ) {
-		actions.push( ...'e'.repeat( oldChildrenLength - oldChildrenHandled ).split( '' ) );
+		for ( let i = 0; i < oldChildrenLength - oldChildrenHandled - offset; i++ ) {
+			actions.push( 'e' );
+		}
 	}
 
 	return actions;

+ 1 - 7
packages/ckeditor5-engine/src/model/documentselection.js

@@ -17,7 +17,6 @@ import TextProxy from './textproxy';
 import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-import log from '@ckeditor/ckeditor5-utils/src/log';
 import uid from '@ckeditor/ckeditor5-utils/src/uid';
 
 const storePrefix = 'selection:';
@@ -817,12 +816,7 @@ class LiveSelection extends Selection {
 		this._checkRange( range );
 
 		if ( range.root == this._document.graveyard ) {
-			/**
-			 * Trying to add a Range that is in the graveyard root. Range rejected.
-			 *
-			 * @warning model-selection-range-in-graveyard
-			 */
-			log.warn( 'model-selection-range-in-graveyard: Trying to add a Range that is in the graveyard root. Range rejected.' );
+			// @if CK_DEBUG // console.warn( 'Trying to add a Range that is in the graveyard root. Range rejected.' );
 
 			return;
 		}

+ 8 - 9
packages/ckeditor5-engine/src/model/operation/transform.js

@@ -16,7 +16,6 @@ import Range from '../range';
 import Position from '../position';
 
 import compareArrays from '@ckeditor/ckeditor5-utils/src/comparearrays';
-import log from '@ckeditor/ckeditor5-utils/src/log';
 
 const transformations = new Map();
 
@@ -102,14 +101,14 @@ export function transform( a, b, context = {} ) {
 
 		return transformationFunction( a, b, context );
 	} catch ( e ) {
-		log.error( 'Error during operation transformation!', e.message );
-		log.error( 'Transformed operation', a );
-		log.error( 'Operation transformed by', b );
-		log.error( 'context.aIsStrong', context.aIsStrong );
-		log.error( 'context.aWasUndone', context.aWasUndone );
-		log.error( 'context.bWasUndone', context.bWasUndone );
-		log.error( 'context.abRelation', context.abRelation );
-		log.error( 'context.baRelation', context.baRelation );
+		// @if CK_DEBUG // console.error( 'Error during operation transformation!', e.message );
+		// @if CK_DEBUG // console.error( 'Transformed operation', a );
+		// @if CK_DEBUG // console.error( 'Operation transformed by', b );
+		// @if CK_DEBUG // console.error( 'context.aIsStrong', context.aIsStrong );
+		// @if CK_DEBUG // console.error( 'context.aWasUndone', context.aWasUndone );
+		// @if CK_DEBUG // console.error( 'context.bWasUndone', context.bWasUndone );
+		// @if CK_DEBUG // console.error( 'context.abRelation', context.abRelation );
+		// @if CK_DEBUG // console.error( 'context.baRelation', context.baRelation );
 
 		throw e;
 	}

+ 2 - 1
packages/ckeditor5-engine/src/model/schema.js

@@ -140,7 +140,7 @@ export default class Schema {
 			 * {@link #register registered} yet.
 			 *
 			 * @param itemName The name of the model element which is being extended.
-			 * @error schema-cannot-register-item-twice
+			 * @error schema-cannot-extend-missing-item
 			 */
 			throw new CKEditorError( 'schema-cannot-extend-missing-item: Cannot extend an item which was not registered yet.', this, {
 				itemName
@@ -1363,6 +1363,7 @@ export class SchemaContext {
  * @typedef {Object} module:engine/model/schema~AttributeProperties
  * @property {Boolean} [isFormatting] Indicates that the attribute should be considered as a visual formatting, like `bold`, `italic` or
  * `fontSize` rather than semantic attribute (such as `src`, `listType`, etc.). For example, it is used by the "Remove format" feature.
+ * @property {Boolean} [copyOnEnter] Indicates that given text attribute should be copied to the next block when enter is pressed.
  */
 
 function compileBaseItemRule( sourceItemRules, itemName ) {

+ 21 - 13
packages/ckeditor5-engine/src/model/utils/insertcontent.js

@@ -11,9 +11,9 @@ import Position from '../position';
 import LivePosition from '../liveposition';
 import Element from '../element';
 import Range from '../range';
-import log from '@ckeditor/ckeditor5-utils/src/log';
 import DocumentSelection from '../documentselection';
 import Selection from '../selection';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 /**
  * Inserts content into the editor (specified selection) as one would expect the paste
@@ -85,13 +85,8 @@ export default function insertContent( model, content, selectable, placeOrOffset
 		} else {
 			// We are not testing else because it's a safe check for unpredictable edge cases:
 			// an insertion without proper range to select.
-
-			/**
-			 * Cannot determine a proper selection range after insertion.
-			 *
-			 * @warning insertcontent-no-range
-			 */
-			log.warn( 'insertcontent-no-range: Cannot determine a proper selection range after insertion.' );
+			//
+			// @if CK_DEBUG // console.warn( 'Cannot determine a proper selection range after insertion.' );
 		}
 
 		const affectedRange = insertion.getAffectedRange() || model.createRange( insertionPosition );
@@ -322,12 +317,19 @@ class Insertion {
 		if ( !this.schema.checkChild( this.position, node ) ) {
 			// Algorithm's correctness check. We should never end up here but it's good to know that we did.
 			// Note that it would often be a silent issue if we insert node in a place where it's not allowed.
-			log.error(
-				'insertcontent-wrong-position: The node cannot be inserted on the given position.',
+
+			/**
+			 * Given node cannot be inserted on the given position.
+			 *
+			 * @error insertcontent-wrong-position
+			 * @param {module:engine/model/node~Node} node Node to insert.
+			 * @param {module:engine/model/position~Position} position Position to insert the node at.
+			 */
+			throw new CKEditorError(
+				'insertcontent-wrong-position: Given node cannot be inserted on the given position.',
+				this,
 				{ node, position: this.position }
 			);
-
-			return;
 		}
 
 		const livePos = LivePosition.fromPosition( this.position, 'toNext' );
@@ -442,7 +444,13 @@ class Insertion {
 				// Algorithm's correctness check. We should never end up here but it's good to know that we did.
 				// At this point the insertion position should be after the node we'll merge. If it isn't,
 				// it should need to be secured as in the left merge case.
-				log.error( 'insertcontent-wrong-position-on-merge: The insertion position should equal the merge position' );
+				/**
+				 * An internal error occured during merging insertion content with siblings.
+				 * The insertion position should equal to the merge position.
+				 *
+				 * @error insertcontent-invalid-insertion-position
+				 */
+				throw new CKEditorError( 'insertcontent-invalid-insertion-position', this );
 			}
 
 			// Move the position to the previous node, so it isn't moved to the graveyard on merge.

+ 1 - 1
packages/ckeditor5-engine/src/model/writer.js

@@ -1010,7 +1010,7 @@ export default class Writer {
 	 *		updateMarker( markerName, { affectsData: false } );
 	 *
 	 * @see module:engine/model/markercollection~Marker
-	 * @param {String} markerOrName Name of a marker to update, or a marker instance.
+	 * @param {String|module:engine/model/markercollection~Marker} markerOrName Name of a marker to update, or a marker instance.
 	 * @param {Object} [options] If options object is not defined then marker will be refreshed by triggering
 	 * downcast conversion for this marker with the same data.
 	 * @param {module:engine/model/range~Range} [options.range] Marker range to update.

+ 35 - 9
packages/ckeditor5-engine/src/view/document.js

@@ -100,23 +100,49 @@ export default class Document {
 	}
 
 	/**
-	 * Used to register a post-fixer callback. A post-fixers mechanism allows to update view tree just before rendering
+	 * Allows registering post-fixer callbacks. A post-fixers mechanism allows to update the view tree just before it is rendered
 	 * to the DOM.
 	 *
-	 * Post-fixers are fired just after all changes from the outermost change block were applied but
+	 * Post-fixers are executed right after all changes from the outermost change block were applied but
 	 * before the {@link module:engine/view/view~View#event:render render event} is fired. If a post-fixer callback made
 	 * a change, it should return `true`. When this happens, all post-fixers are fired again to check if something else should
 	 * not be fixed in the new document tree state.
 	 *
-	 * View post-fixers are useful when you wants to update view structure whenever it changes, for instance add some classes
-	 * to elements based on the view structure or selection. However, is you need DOM elements to be already updated, use
-	 * {@link module:engine/view/view~View#event:render render event}.
+	 * View post-fixers are useful when you want to apply some fixes whenever the view structure changes. Keep in mind that
+	 * changes executed in a view post-fixer should not break model-view mapping.
 	 *
-	 * As a parameter, a post-fixer callback receives a {@link module:engine/view/downcastwriter~DowncastWriter downcast writer}
-	 * instance connected with the executed changes block.
+	 * The types of changes which should be safe:
 	 *
-	 * Note that registering a post-fixer won't re-render the editor's view. If the view should change after registering the post-fixer then
-	 * it should be done manually calling `view.forceRender();`.
+	 * * adding or removing attribute from elements,
+	 * * changes inside of {@link module:engine/view/uielement~UIElement UI elements},
+	 * * {@link module:engine/model/differ~Differ#refreshItem marking some of the model elements to be re-converted}.
+	 *
+	 * Try to avoid changes which touch view structure:
+	 *
+	 * * you should not add or remove nor wrap or unwrap any view elements,
+	 * * you should not change the editor data model in a view post-fixer.
+	 *
+	 * As a parameter, a post-fixer callback receives a {@link module:engine/view/downcastwriter~DowncastWriter downcast writer}.
+	 *
+	 * Typically, a post-fixer will look like this:
+	 *
+	 *		editor.editing.view.document.registerPostFixer( writer => {
+	 *			if ( checkSomeCondition() ) {
+	 *				writer.doSomething();
+	 *
+	 *				// Let other post-fixers know that something changed.
+	 *				return true;
+	 *			}
+	 *		} );
+	 *
+	 * Note that nothing happens right after you register a post-fixer (e.g. execute such a code in the console).
+	 * That is because adding a post-fixer does not execute it.
+	 * The post-fixer will be executed as soon as any change in the document needs to cause its rendering.
+	 * If you want to re-render the editor's view after registering the post-fixer then you should do it manually by calling
+	 * {@link module:engine/view/view~View#forceRender `view.forceRender()`}.
+	 *
+	 * If you need to register a callback which is executed when DOM elements are already updated,
+	 * use {@link module:engine/view/view~View#event:render render event}.
 	 *
 	 * @param {Function} postFixer
 	 */

+ 6 - 10
packages/ckeditor5-engine/src/view/observer/selectionobserver.js

@@ -11,7 +11,6 @@
 
 import Observer from './observer';
 import MutationObserver from './mutationobserver';
-import log from '@ckeditor/ckeditor5-utils/src/log';
 import { debounce } from 'lodash-es';
 
 /**
@@ -150,15 +149,12 @@ export default class SelectionObserver extends Observer {
 		// This counter is reset each second. 60 selection changes in 1 second is enough high number
 		// to be very difficult (impossible) to achieve using just keyboard keys (during normal editor use).
 		if ( ++this._loopbackCounter > 60 ) {
-			/**
-			 * Selection change observer detected an infinite rendering loop.
-			 * Most probably you try to put the selection in the position which is not allowed
-			 * by the browser and browser fixes it automatically what causes `selectionchange` event on
-			 * which a loopback through a model tries to re-render the wrong selection and again.
-			 *
-			 * @error selectionchange-infinite-loop
-			 */
-			log.warn( 'selectionchange-infinite-loop: Selection change observer detected an infinite rendering loop.' );
+			// Selection change observer detected an infinite rendering loop.
+			// Most probably you try to put the selection in the position which is not allowed
+			// by the browser and browser fixes it automatically what causes `selectionchange` event on
+			// which a loopback through a model tries to re-render the wrong selection and again.
+			//
+			// @if CK_DEBUG // console.warn( 'Selection change observer detected an infinite rendering loop.' );
 
 			return;
 		}

+ 5 - 9
packages/ckeditor5-engine/src/view/view.js

@@ -24,7 +24,6 @@ import CompositionObserver from './observer/compositionobserver';
 import InputObserver from './observer/inputobserver';
 
 import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
-import log from '@ckeditor/ckeditor5-utils/src/log';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import { scrollViewportToShowTarget } from '@ckeditor/ckeditor5-utils/src/dom/scroll';
 import { injectUiElementHandling } from './uielement';
@@ -398,14 +397,11 @@ export default class View {
 				this.domConverter.focus( editable );
 				this.forceRender();
 			} else {
-				/**
-				 * Before focusing view document, selection should be placed inside one of the view's editables.
-				 * Normally its selection will be converted from model document (which have default selection), but
-				 * when using view document on its own, we need to manually place selection before focusing it.
-				 *
-				 * @error view-focus-no-selection
-				 */
-				log.warn( 'view-focus-no-selection: There is no selection in any editable to focus.' );
+				// Before focusing view document, selection should be placed inside one of the view's editables.
+				// Normally its selection will be converted from model document (which have default selection), but
+				// when using view document on its own, we need to manually place selection before focusing it.
+				//
+				// @if CK_DEBUG // console.warn( 'There is no selection in any editable to focus.' );
 			}
 		}
 	}

+ 1 - 1
packages/ckeditor5-engine/tests/conversion/conversion.js

@@ -81,7 +81,7 @@ describe( 'Conversion', () => {
 
 		it( 'should be chainable', () => {
 			const helpers = conversion.for( 'upcast' );
-			const addResult = helpers.add( () => { } );
+			const addResult = helpers.add( () => {} );
 
 			expect( addResult ).to.equal( helpers );
 		} );

+ 2 - 2
packages/ckeditor5-engine/tests/conversion/downcastdispatcher.js

@@ -392,8 +392,8 @@ describe( 'DowncastDispatcher', () => {
 			const viewFigure = new ViewContainerElement( 'figure', null, viewCaption );
 
 			// Create custom highlight handler mock.
-			viewFigure._setCustomProperty( 'addHighlight', () => { } );
-			viewFigure._setCustomProperty( 'removeHighlight', () => { } );
+			viewFigure._setCustomProperty( 'addHighlight', () => {} );
+			viewFigure._setCustomProperty( 'removeHighlight', () => {} );
 
 			// Create mapper mock.
 			dispatcher.conversionApi.mapper = {

+ 13 - 21
packages/ckeditor5-engine/tests/conversion/downcasthelpers.js

@@ -3,6 +3,8 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
+/* globals console */
+
 import EditingController from '../../src/controller/editingcontroller';
 
 import Model from '../../src/model/model';
@@ -15,7 +17,6 @@ import ViewContainerElement from '../../src/view/containerelement';
 import ViewUIElement from '../../src/view/uielement';
 import ViewText from '../../src/view/text';
 
-import log from '@ckeditor/ckeditor5-utils/src/log';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 
 import DowncastHelpers, {
@@ -32,6 +33,7 @@ import { stringify as stringifyView } from '../../src/dev-utils/view';
 import View from '../../src/view/view';
 import createViewRoot from '../view/_utils/createroot';
 import { setData as setModelData } from '../../src/dev-utils/model';
+import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'DowncastHelpers', () => {
 	let model, modelRoot, viewRoot, downcastHelpers, controller;
@@ -614,7 +616,7 @@ describe( 'DowncastHelpers', () => {
 
 		// #1587
 		it( 'config.view and config.model as strings in generic conversion (elements only)', () => {
-			const logStub = testUtils.sinon.stub( log, 'warn' );
+			const consoleWarnStub = testUtils.sinon.stub( console, 'warn' );
 
 			downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
 
@@ -626,7 +628,7 @@ describe( 'DowncastHelpers', () => {
 			} );
 
 			expectResult( '<p test="1"></p><p test="2"></p>' );
-			expect( logStub.callCount ).to.equal( 0 );
+			expect( consoleWarnStub.callCount ).to.equal( 0 );
 
 			model.change( writer => {
 				writer.removeAttribute( 'test', modelRoot.getChild( 1 ) );
@@ -637,29 +639,19 @@ describe( 'DowncastHelpers', () => {
 
 		// #1587
 		it( 'config.view and config.model as strings in generic conversion (elements + text)', () => {
-			const logStub = testUtils.sinon.stub( log, 'warn' );
-
 			downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
 
 			downcastHelpers.attributeToAttribute( { model: 'test', view: 'test' } );
 
-			model.change( writer => {
-				writer.insertElement( 'paragraph', modelRoot, 0 );
-				writer.insertElement( 'paragraph', { test: '1' }, modelRoot, 1 );
-
-				writer.insertText( 'Foo', { test: '2' }, modelRoot.getChild( 0 ), 0 );
-				writer.insertText( 'Bar', { test: '3' }, modelRoot.getChild( 1 ), 0 );
-			} );
-
-			expectResult( '<p>Foo</p><p test="1">Bar</p>' );
-			expect( logStub.callCount ).to.equal( 2 );
-			expect( logStub.alwaysCalledWithMatch( 'conversion-attribute-to-attribute-on-text' ) ).to.true;
-
-			model.change( writer => {
-				writer.removeAttribute( 'test', modelRoot.getChild( 1 ) );
-			} );
+			expectToThrowCKEditorError( () => {
+				model.change( writer => {
+					writer.insertElement( 'paragraph', modelRoot, 0 );
+					writer.insertElement( 'paragraph', { test: '1' }, modelRoot, 1 );
 
-			expectResult( '<p>Foo</p><p>Bar</p>' );
+					writer.insertText( 'Foo', { test: '2' }, modelRoot.getChild( 0 ), 0 );
+					writer.insertText( 'Bar', { test: '3' }, modelRoot.getChild( 1 ), 0 );
+				} );
+			}, /^conversion-attribute-to-attribute-on-text/ );
 		} );
 
 		it( 'should convert attribute insert/change/remove on a model node', () => {

+ 11 - 6
packages/ckeditor5-engine/tests/conversion/mapper.js

@@ -643,20 +643,25 @@ describe( 'Mapper', () => {
 			expect( elements ).to.deep.equal( [ viewA, viewB, viewC ] );
 		} );
 
-		it( 'should unbind all elements from a marker name', () => {
+		it( 'should unbind element from a marker name', () => {
 			const viewA = new ViewElement( 'a' );
 			const viewB = new ViewElement( 'b' );
-			const viewC = new ViewElement( 'c' );
 
 			mapper.bindElementToMarker( viewA, 'marker' );
 			mapper.bindElementToMarker( viewB, 'marker' );
-			mapper.bindElementToMarker( viewC, 'marker' );
 
-			mapper.unbindElementsFromMarkerName( 'marker' );
+			mapper.unbindElementFromMarkerName( viewA, 'marker' );
 
-			const elements = mapper.markerNameToElements( 'marker' );
+			expect( Array.from( mapper.markerNameToElements( 'marker' ) ) ).to.deep.equal( [ viewB ] );
+
+			mapper.unbindElementFromMarkerName( viewB, 'marker' );
+
+			expect( mapper.markerNameToElements( 'marker' ) ).to.be.null;
+
+			// Removing an element from non-existing group or non-bound element should not cause a crash.
+			mapper.unbindElementFromMarkerName( viewB, 'marker' );
 
-			expect( elements ).to.be.null;
+			expect( mapper.markerNameToElements( 'marker' ) ).to.be.null;
 		} );
 	} );
 

+ 100 - 0
packages/ckeditor5-engine/tests/model/differ.js

@@ -1243,6 +1243,36 @@ describe( 'Differ', () => {
 			} );
 		} );
 
+		it( 'on an element that got some nodes inserted', () => {
+			const p = root.getChild( 0 );
+
+			model.change( () => {
+				insert( new Text( 'x' ), Position._createAt( p, 3 ) );
+				insert( new Text( 'x' ), Position._createAt( p, 4 ) );
+				insert( new Text( 'x' ), Position._createAt( p, 5 ) );
+
+				attribute( new Range( Position._createAt( root, 0 ), Position._createAt( root, 1 ) ), 'a', null, true );
+
+				insert( new Text( 'y' ), Position._createAt( p, 6 ) );
+
+				expectChanges( [
+					{
+						type: 'attribute',
+						range: new Range( Position._createAt( root, 0 ), Position._createAt( p, 0 ) ),
+						attributeKey: 'a',
+						attributeOldValue: null,
+						attributeNewValue: true
+					},
+					{
+						type: 'insert',
+						position: Position._createAt( p, 3 ),
+						length: 4,
+						name: '$text'
+					}
+				] );
+			} );
+		} );
+
 		it( 'over all changed nodes and some not changed nodes', () => {
 			const p = root.getChild( 0 );
 
@@ -1759,6 +1789,76 @@ describe( 'Differ', () => {
 		} );
 	} );
 
+	describe( 'refreshItem()', () => {
+		it( 'should mark given element to be removed and added again', () => {
+			const p = root.getChild( 0 );
+
+			differ.refreshItem( p );
+
+			expectChanges( [
+				{ type: 'remove', name: 'paragraph', length: 1, position: model.createPositionBefore( p ) },
+				{ type: 'insert', name: 'paragraph', length: 1, position: model.createPositionBefore( p ) }
+			], true );
+		} );
+
+		it( 'should mark given text proxy to be removed and added again', () => {
+			const p = root.getChild( 0 );
+			const range = model.createRangeIn( p );
+			const textProxy = [ ...range.getItems() ][ 0 ];
+
+			differ.refreshItem( textProxy );
+
+			expectChanges( [
+				{ type: 'remove', name: '$text', length: 3, position: model.createPositionAt( p, 0 ) },
+				{ type: 'insert', name: '$text', length: 3, position: model.createPositionAt( p, 0 ) }
+			], true );
+		} );
+
+		it( 'inside a new element', () => {
+			// Since the refreshed element is inside a new element, it should not be listed on changes list.
+			model.change( () => {
+				insert( new Element( 'blockQuote', null, new Element( 'paragraph' ) ), new Position( root, [ 2 ] ) );
+
+				differ.refreshItem( root.getChild( 2 ).getChild( 0 ) );
+
+				expectChanges( [
+					{ type: 'insert', name: 'blockQuote', length: 1, position: new Position( root, [ 2 ] ) }
+				] );
+			} );
+		} );
+
+		it( 'markers refreshing', () => {
+			model.change( () => {
+				// Refreshed element contains marker.
+				model.markers._set( 'markerA', new Range( new Position( root, [ 1, 1 ] ), new Position( root, [ 1, 2 ] ) ) );
+
+				// Marker contains refreshed element.
+				model.markers._set( 'markerB', new Range( new Position( root, [ 0 ] ), new Position( root, [ 2 ] ) ) );
+
+				// Intersecting.
+				model.markers._set( 'markerC', new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 1, 2 ] ) ) );
+
+				// Not intersecting.
+				model.markers._set( 'markerD', new Range( new Position( root, [ 0, 0 ] ), new Position( root, [ 1 ] ) ) );
+			} );
+
+			const markersToRefresh = [ 'markerA', 'markerB', 'markerC' ];
+
+			differ.refreshItem( root.getChild( 1 ) );
+
+			expectChanges( [
+				{ type: 'remove', name: 'paragraph', length: 1, position: new Position( root, [ 1 ] ) },
+				{ type: 'insert', name: 'paragraph', length: 1, position: new Position( root, [ 1 ] ) }
+			] );
+
+			const markersToRemove = differ.getMarkersToRemove().map( entry => entry.name );
+			const markersToAdd = differ.getMarkersToAdd().map( entry => entry.name );
+
+			expect( markersToRefresh ).to.deep.equal( markersToRemove );
+			expect( markersToRefresh ).to.deep.equal( markersToAdd );
+		} );
+	} );
+
 	describe( 'getChanges()', () => {
 		let position, p1, rangeAttrChange, range;
 

+ 2 - 2
packages/ckeditor5-engine/tests/model/document.js

@@ -44,7 +44,7 @@ describe( 'Document', () => {
 				baseVersion: 0,
 				isDocumentOperation: true,
 				_execute: sinon.stub().returns( data ),
-				_validate: () => { }
+				_validate: () => {}
 			};
 
 			batch = new Batch();
@@ -82,7 +82,7 @@ describe( 'Document', () => {
 			const operation = {
 				baseVersion: 1,
 				isDocumentOperation: true,
-				_execute: () => { }
+				_execute: () => {}
 			};
 
 			expectToThrowCKEditorError( () => {

+ 8 - 24
packages/ckeditor5-engine/tests/model/documentselection.js

@@ -17,15 +17,12 @@ import AttributeOperation from '../../src/model/operation/attributeoperation';
 import SplitOperation from '../../src/model/operation/splitoperation';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import count from '@ckeditor/ckeditor5-utils/src/count';
-import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import { setData, getData } from '../../src/dev-utils/model';
 import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 
 describe( 'DocumentSelection', () => {
 	let model, doc, root, selection, liveRange, range;
 
-	testUtils.createSinonSandbox();
-
 	const fooStoreAttrKey = DocumentSelection._getStoreAttributeKey( 'foo' );
 	const abcStoreAttrKey = DocumentSelection._getStoreAttributeKey( 'abc' );
 
@@ -54,6 +51,7 @@ describe( 'DocumentSelection', () => {
 	} );
 
 	afterEach( () => {
+		sinon.restore();
 		model.destroy();
 		liveRange.detach();
 	} );
@@ -457,9 +455,6 @@ describe( 'DocumentSelection', () => {
 
 			expect( ranges[ 0 ].detach.called ).to.be.true;
 			expect( ranges[ 1 ].detach.called ).to.be.true;
-
-			ranges[ 0 ].detach.restore();
-			ranges[ 1 ].detach.restore();
 		} );
 	} );
 
@@ -518,7 +513,7 @@ describe( 'DocumentSelection', () => {
 		it( 'detaches all existing ranges', () => {
 			selection._setTo( [ range, liveRange ] );
 
-			const spy = testUtils.sinon.spy( LiveRange.prototype, 'detach' );
+			const spy = sinon.spy( LiveRange.prototype, 'detach' );
 			selection._setTo( root, 0 );
 
 			expect( spy.calledTwice ).to.be.true;
@@ -540,7 +535,7 @@ describe( 'DocumentSelection', () => {
 			const startPos = Position._createAt( root, 1 );
 			const endPos = Position._createAt( root, 2 );
 			const newEndPos = Position._createAt( root, 4 );
-			const spy = testUtils.sinon.spy( LiveRange.prototype, 'detach' );
+			const spy = sinon.spy( LiveRange.prototype, 'detach' );
 
 			selection._setTo( new Range( startPos, endPos ) );
 
@@ -575,11 +570,6 @@ describe( 'DocumentSelection', () => {
 			selection._setTo( null );
 		} );
 
-		afterEach( () => {
-			ranges[ 0 ].detach.restore();
-			ranges[ 1 ].detach.restore();
-		} );
-
 		it( 'should remove all stored ranges (and reset to default range)', () => {
 			expect( Array.from( selection.getRanges() ).length ).to.equal( 1 );
 			expect( selection.anchor.isEqual( new Position( root, [ 0, 0 ] ) ) ).to.be.true;
@@ -607,19 +597,13 @@ describe( 'DocumentSelection', () => {
 			}, /model-selection-set-ranges-not-range/, model );
 		} );
 
-		it( 'should log a warning when trying to set selection to the graveyard', () => {
-			// eslint-disable-next-line no-undef
-			const warnStub = sinon.stub( console, 'warn' );
-
-			const range = new Range( new Position( model.document.graveyard, [ 0 ] ) );
-			selection._setTo( range );
-
-			expect( warnStub.calledOnce ).to.equal( true );
-			expect( warnStub.getCall( 0 ).args[ 0 ] ).to.match( /model-selection-range-in-graveyard/ );
+		it( 'should not do nothing when trying to set selection to the graveyard', () => {
+			expect( () => {
+				const range = new Range( new Position( model.document.graveyard, [ 0 ] ) );
+				selection._setTo( range );
+			} ).to.not.throw();
 
 			expect( selection._ranges ).to.deep.equal( [] );
-
-			warnStub.restore();
 		} );
 
 		it( 'should detach removed ranges', () => {

+ 5 - 9
packages/ckeditor5-engine/tests/model/operation/transform.js

@@ -19,8 +19,6 @@ import MoveOperation from '../../../src/model/operation/moveoperation';
 import RenameOperation from '../../../src/model/operation/renameoperation';
 import NoOperation from '../../../src/model/operation/nooperation';
 
-import log from '@ckeditor/ckeditor5-utils/src/log';
-
 describe( 'transform', () => {
 	let model, doc, root, op, nodeA, nodeB, expected;
 
@@ -33,6 +31,10 @@ describe( 'transform', () => {
 		nodeB = new Node();
 	} );
 
+	afterEach( () => {
+		sinon.restore();
+	} );
+
 	function expectOperation( op, params ) {
 		for ( const i in params ) {
 			if ( params.hasOwnProperty( i ) ) {
@@ -58,9 +60,7 @@ describe( 'transform', () => {
 		aIsStrong: true
 	};
 
-	it( 'error logging', () => {
-		const spy = sinon.stub( log, 'error' );
-
+	it( 'should throw an error when one of operations is invalid', () => {
 		const nodeA = new Node();
 		const nodeB = new Node();
 
@@ -81,10 +81,6 @@ describe( 'transform', () => {
 				baRelation: null
 			} );
 		} ).to.throw();
-
-		sinon.assert.called( spy );
-
-		log.error.restore();
 	} );
 
 	describe( 'InsertOperation', () => {

+ 13 - 19
packages/ckeditor5-engine/tests/view/observer/selectionobserver.js

@@ -3,24 +3,20 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
-/* globals setTimeout, document */
+/* globals setTimeout, document, console */
 
 import ViewRange from '../../../src/view/range';
-import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import DocumentSelection from '../../../src/view/documentselection';
 import ViewSelection from '../../../src/view/selection';
 import View from '../../../src/view/view';
 import SelectionObserver from '../../../src/view/observer/selectionobserver';
 import FocusObserver from '../../../src/view/observer/focusobserver';
-import log from '@ckeditor/ckeditor5-utils/src/log';
 import createViewRoot from '../_utils/createroot';
 import { parse } from '../../../src/dev-utils/view';
 
 describe( 'SelectionObserver', () => {
 	let view, viewDocument, viewRoot, selectionObserver, domRoot, domMain, domDocument;
 
-	testUtils.createSinonSandbox();
-
 	beforeEach( done => {
 		domDocument = document;
 		domRoot = domDocument.createElement( 'div' );
@@ -56,6 +52,7 @@ describe( 'SelectionObserver', () => {
 	} );
 
 	afterEach( () => {
+		sinon.restore();
 		domRoot.parentElement.removeChild( domRoot );
 
 		view.destroy();
@@ -162,7 +159,7 @@ describe( 'SelectionObserver', () => {
 		changeDomSelection();
 	} );
 
-	it( 'should warn and not enter infinite loop', () => {
+	it( 'should not enter infinite loop', () => {
 		let counter = 70;
 
 		const viewFoo = viewDocument.getRoot().getChild( 0 ).getChild( 0 );
@@ -170,17 +167,14 @@ describe( 'SelectionObserver', () => {
 			writer.setSelection( viewFoo, 0 );
 		} );
 
-		return new Promise( ( resolve, reject ) => {
-			testUtils.sinon.stub( log, 'warn' ).callsFake( msg => {
-				expect( msg ).to.match( /^selectionchange-infinite-loop/ );
+		const selectionChangeSpy = sinon.spy();
 
-				resolve();
-			} );
+		viewDocument.on( 'selectionChange', selectionChangeSpy );
 
+		return new Promise( resolve => {
 			viewDocument.on( 'selectionChangeDone', () => {
-				if ( !counter ) {
-					reject( new Error( 'Infinite loop warning was not logged.' ) );
-				}
+				expect( selectionChangeSpy.callCount ).to.equal( 60 );
+				resolve();
 			} );
 
 			while ( counter > 0 ) {
@@ -193,10 +187,10 @@ describe( 'SelectionObserver', () => {
 	it( 'should not be treated as an infinite loop if selection is changed only few times', done => {
 		const viewFoo = viewDocument.getRoot().getChild( 0 ).getChild( 0 );
 		viewDocument.selection._setTo( ViewRange._createFromParentsAndOffsets( viewFoo, 0, viewFoo, 0 ) );
-		const spy = testUtils.sinon.spy( log, 'warn' );
+		const consoleWarnSpy = sinon.spy( console, 'warn' );
 
 		viewDocument.on( 'selectionChangeDone', () => {
-			expect( spy.called ).to.be.false;
+			expect( consoleWarnSpy.called ).to.be.false;
 			done();
 		} );
 
@@ -206,10 +200,10 @@ describe( 'SelectionObserver', () => {
 	} );
 
 	it( 'should not be treated as an infinite loop if changes are not often', () => {
-		const clock = testUtils.sinon.useFakeTimers( {
+		const clock = sinon.useFakeTimers( {
 			toFake: [ 'setInterval', 'clearInterval' ]
 		} );
-		const stub = testUtils.sinon.stub( log, 'warn' );
+		const consoleWarnStub = sinon.stub( console, 'warn' );
 
 		// We need to recreate SelectionObserver, so it will use mocked setInterval.
 		selectionObserver.disable();
@@ -220,7 +214,7 @@ describe( 'SelectionObserver', () => {
 		return doChanges()
 			.then( doChanges )
 			.then( () => {
-				sinon.assert.notCalled( stub );
+				sinon.assert.notCalled( consoleWarnStub );
 				clock.restore();
 			} );
 

+ 16 - 20
packages/ckeditor5-engine/tests/view/view/view.js

@@ -3,7 +3,7 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
-/* globals document */
+/* globals document, console */
 
 import View from '../../../src/view/view';
 import Observer from '../../../src/view/observer/observer';
@@ -19,13 +19,11 @@ import ViewElement from '../../../src/view/element';
 import ViewPosition from '../../../src/view/position';
 import ViewSelection from '../../../src/view/selection';
 import { isBlockFiller, BR_FILLER } from '../../../src/view/filler';
-import log from '@ckeditor/ckeditor5-utils/src/log';
 
 import count from '@ckeditor/ckeditor5-utils/src/count';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 import createViewRoot from '../_utils/createroot';
 import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
-import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 import env from '@ckeditor/ckeditor5-utils/src/env';
 
@@ -33,8 +31,6 @@ describe( 'view', () => {
 	const DEFAULT_OBSERVERS_COUNT = 6;
 	let domRoot, view, viewDocument, ObserverMock, instantiated, enabled, ObserverMockGlobalCount;
 
-	testUtils.createSinonSandbox();
-
 	beforeEach( () => {
 		domRoot = createElement( document, 'div', {
 			id: 'editor',
@@ -75,6 +71,7 @@ describe( 'view', () => {
 	} );
 
 	afterEach( () => {
+		sinon.restore();
 		domRoot.remove();
 		view.destroy();
 	} );
@@ -366,11 +363,11 @@ describe( 'view', () => {
 	describe( 'scrollToTheSelection()', () => {
 		beforeEach( () => {
 			// Silence the Rect warnings.
-			testUtils.sinon.stub( log, 'warn' );
+			sinon.stub( console, 'warn' );
 		} );
 
 		it( 'does nothing when there are no ranges in the selection', () => {
-			const stub = testUtils.sinon.stub( global.window, 'scrollTo' );
+			const stub = sinon.stub( global.window, 'scrollTo' );
 
 			view.scrollToTheSelection();
 			sinon.assert.notCalled( stub );
@@ -378,7 +375,7 @@ describe( 'view', () => {
 
 		it( 'scrolls to the first range in selection with an offset', () => {
 			const root = createViewRoot( viewDocument, 'div', 'main' );
-			const stub = testUtils.sinon.stub( global.window, 'scrollTo' );
+			const stub = sinon.stub( global.window, 'scrollTo' );
 			const range = ViewRange._createIn( root );
 
 			view.attachDomRoot( domRoot );
@@ -449,8 +446,8 @@ describe( 'view', () => {
 		} );
 
 		it( 'should focus editable with selection', () => {
-			const converterFocusSpy = testUtils.sinon.spy( view.domConverter, 'focus' );
-			const renderSpy = testUtils.sinon.spy( view, 'forceRender' );
+			const converterFocusSpy = sinon.spy( view.domConverter, 'focus' );
+			const renderSpy = sinon.spy( view, 'forceRender' );
 
 			view.focus();
 
@@ -466,8 +463,8 @@ describe( 'view', () => {
 		} );
 
 		it( 'should not focus if document is already focused', () => {
-			const converterFocusSpy = testUtils.sinon.spy( view.domConverter, 'focus' );
-			const renderSpy = testUtils.sinon.spy( view, 'forceRender' );
+			const converterFocusSpy = sinon.spy( view.domConverter, 'focus' );
+			const renderSpy = sinon.spy( view, 'forceRender' );
 			viewDocument.isFocused = true;
 
 			view.focus();
@@ -476,15 +473,14 @@ describe( 'view', () => {
 			expect( renderSpy.called ).to.be.false;
 		} );
 
-		it( 'should log warning when no selection', () => {
-			const logSpy = testUtils.sinon.stub( log, 'warn' );
-			view.change( writer => {
-				writer.setSelection( null );
-			} );
+		it( 'should not crash when there is no selection', () => {
+			expect( () => {
+				view.change( writer => {
+					writer.setSelection( null );
+				} );
 
-			view.focus();
-			expect( logSpy.calledOnce ).to.be.true;
-			expect( logSpy.args[ 0 ][ 0 ] ).to.match( /^view-focus-no-selection/ );
+				view.focus();
+			} ).not.to.throw();
 		} );
 	} );