浏览代码

Merge pull request #88 from cksource/t/69

t/69: Transaction and deltas.
Piotrek Koszuliński 10 年之前
父节点
当前提交
932c21b0e4
共有 31 个文件被更改,包括 1551 次插入46 次删除
  1. 155 0
      packages/ckeditor5-engine/src/document/delta/changedelta.js
  2. 58 0
      packages/ckeditor5-engine/src/document/delta/delta.js
  3. 44 0
      packages/ckeditor5-engine/src/document/delta/insertdelta.js
  4. 78 0
      packages/ckeditor5-engine/src/document/delta/mergedelta.js
  5. 14 0
      packages/ckeditor5-engine/src/document/delta/register.js
  6. 47 0
      packages/ckeditor5-engine/src/document/delta/removedelta.js
  7. 71 0
      packages/ckeditor5-engine/src/document/delta/splitdelta.js
  8. 124 0
      packages/ckeditor5-engine/src/document/delta/transaction-base.js
  9. 24 4
      packages/ckeditor5-engine/src/document/document.js
  10. 6 1
      packages/ckeditor5-engine/src/document/element.js
  11. 6 11
      packages/ckeditor5-engine/src/document/node.js
  12. 8 0
      packages/ckeditor5-engine/src/document/operation/operation.js
  13. 3 0
      packages/ckeditor5-engine/src/document/positioniterator.js
  14. 27 1
      packages/ckeditor5-engine/src/document/range.js
  15. 21 0
      packages/ckeditor5-engine/src/document/transaction.js
  16. 18 0
      packages/ckeditor5-engine/tests/_tools/tools.js
  17. 7 0
      packages/ckeditor5-engine/tests/bender/tools.js
  18. 6 2
      packages/ckeditor5-engine/tests/document/character.js
  19. 275 0
      packages/ckeditor5-engine/tests/document/deltas/changedelta.js
  20. 67 0
      packages/ckeditor5-engine/tests/document/deltas/delta.js
  21. 46 0
      packages/ckeditor5-engine/tests/document/deltas/insertdelta.js
  22. 80 0
      packages/ckeditor5-engine/tests/document/deltas/mergedelta.js
  23. 61 0
      packages/ckeditor5-engine/tests/document/deltas/removedelta.js
  24. 101 0
      packages/ckeditor5-engine/tests/document/deltas/splitdelta.js
  25. 12 1
      packages/ckeditor5-engine/tests/document/document.js
  26. 6 2
      packages/ckeditor5-engine/tests/document/element.js
  27. 28 5
      packages/ckeditor5-engine/tests/document/node.js
  28. 20 16
      packages/ckeditor5-engine/tests/document/operation/changeoperation.js
  29. 51 2
      packages/ckeditor5-engine/tests/document/range.js
  30. 5 1
      packages/ckeditor5-engine/tests/document/rootelement.js
  31. 82 0
      packages/ckeditor5-engine/tests/document/transaction.js

+ 155 - 0
packages/ckeditor5-engine/src/document/delta/changedelta.js

@@ -0,0 +1,155 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [
+	'document/delta/delta',
+	'document/delta/register',
+	'document/operation/changeoperation',
+	'document/position',
+	'document/range',
+	'document/attribute',
+	'document/element'
+], ( Delta, register, ChangeOperation, Position, Range, Attribute, Element ) => {
+	/**
+	 * To provide specific OT behavior and better collisions solving, change methods ({@link document.Transaction#setAttr}
+	 * and {@link document.Transaction#removeAttr}) use `ChangeDelta` class which inherits from the `Delta` class and may
+	 * overwrite some methods.
+	 *
+	 * @class document.delta.ChangeDelta
+	 */
+	class ChangeDelta extends Delta {}
+
+	/**
+	 * Sets the value of the attribute of the node or on the range.
+	 *
+	 * @chainable
+	 * @method setAttr
+	 * @memberOf document.Transaction
+	 * @param {String} key Attribute key.
+	 * @param {Mixed} value Attribute new value.
+	 * @param {document.Node|document.Range} nodeOrRange Node or range on which the attribute will be set.
+	 */
+	register( 'setAttr', function( key, value, nodeOrRange ) {
+		change( this, key, value, nodeOrRange );
+
+		return this;
+	} );
+
+	/**
+	 * Removes an attribute from the range.
+	 *
+	 * @chainable
+	 * @method removeAttr
+	 * @memberOf document.Transaction
+	 * @param {String} key Attribute key.
+	 * @param {document.Node|document.Range} nodeOrRange Node or range on which the attribute will be removed.
+	 */
+	register( 'removeAttr', function( key, nodeOrRange ) {
+		change( this, key, null, nodeOrRange );
+
+		return this;
+	} );
+
+	function change( transaction, key, value, nodeOrRange ) {
+		const delta = new ChangeDelta();
+
+		if ( nodeOrRange instanceof Range ) {
+			changeRange( transaction.doc, delta, key, value, nodeOrRange );
+		} else {
+			changeNode( transaction.doc, delta, key, value, nodeOrRange );
+		}
+
+		transaction.addDelta( delta );
+	}
+
+	function changeNode( doc, delta, key, value, node ) {
+		const previousValue = node.getAttr( key );
+		let range;
+
+		if ( previousValue != value ) {
+			if ( node instanceof Element ) {
+				// If we change the attribute of the element, we do not want to change attributes of its children, so
+				// the end on the range can not be put after the closing tag, it should be inside that element with the
+				// offset 0, so the range will contains only the opening tag...
+				range = new Range( Position.createBefore( node ), Position.createFromParentAndOffset( node, 0 ) );
+			} else {
+				// ...but for characters we can not put the range inside it, so we end the range after that character.
+				range = new Range( Position.createBefore( node ), Position.createAfter( node ) );
+			}
+
+			const operation = new ChangeOperation(
+					range,
+					previousValue ? new Attribute( key, previousValue ) : null,
+					value ? new Attribute( key, value ) : null,
+					doc.version
+				);
+
+			doc.applyOperation( operation );
+			delta.addOperation( operation );
+		}
+	}
+
+	// Because change operation needs to have the same attribute value on the whole range, this function split the range
+	// into smaller parts.
+	function changeRange( doc, delta, key, value, range ) {
+		// Position of the last split, the beginning of the new range.
+		let lastSplitPosition = range.start;
+
+		// Currently position in the scanning range. Because we need value after the position, it is not a current
+		// position of the iterator but the previous one (we need to iterate one more time to get the value after).
+		let position;
+		// Value before the currently position.
+		let valueBefore;
+		// Value after the currently position.
+		let valueAfter;
+
+		// Because we need not only a node, but also a position, we can not use ( value of range ).
+		const iterator = range[ Symbol.iterator ]();
+		// Iterator state.
+		let next = iterator.next();
+
+		while ( !next.done ) {
+			valueAfter = next.value.node.getAttr( key );
+
+			// At the first run of the iterator the position in undefined. We also do not have a valueBefore, but
+			// because valueAfter may be null, valueBefore may be equal valueAfter ( undefined == null ).
+			if ( position && valueBefore != valueAfter ) {
+				// if valueBefore == value there is nothing to change, so we add operation only if these values are different.
+				if ( valueBefore != value ) {
+					addOperation();
+				}
+
+				lastSplitPosition = position;
+			}
+
+			position = iterator.position;
+			valueBefore = valueAfter;
+
+			next = iterator.next();
+		}
+
+		// Because position in the loop is not the iterator position (see let position comment), the last position in
+		// the while loop will be last but one position in the range. We need to check the last position manually.
+		if ( position != lastSplitPosition && valueBefore != value ) {
+			addOperation();
+		}
+
+		function addOperation() {
+			const operation = new ChangeOperation(
+					new Range( lastSplitPosition, position ),
+					valueBefore ? new Attribute( key, valueBefore ) : null,
+					value ? new Attribute( key, value ) : null,
+					doc.version
+				);
+
+			doc.applyOperation( operation );
+			delta.addOperation( operation );
+		}
+	}
+
+	return ChangeDelta;
+} );

+ 58 - 0
packages/ckeditor5-engine/src/document/delta/delta.js

@@ -0,0 +1,58 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [], () => {
+	/**
+	 * Base class for all deltas.
+	 *
+	 * Delta is a single, from the user action point of view, change in the editable document, like insert, split or
+	 * rename element. Delta is composed of operations, which are unit changes needed to be done to execute user action.
+	 *
+	 * Multiple deltas are grouped into a single {@link document.Transaction}.
+	 *
+	 * @class document.delta.Delta
+	 */
+	class Delta {
+		/**
+		 * Creates a delta instance.
+		 *
+		 * @constructor
+		 */
+		constructor() {
+			/**
+			 * {@link document.Transaction} which delta is a part of. This property is null by default and set by the
+			 * {@link Document.Transaction#addDelta} method.
+			 *
+			 * @readonly
+			 * @type {document.Transaction}
+			 */
+			this.transaction = null;
+
+			/**
+			 * Array of operations which compose delta.
+			 *
+			 * @readonly
+			 * @type {document.operation.Operation[]}
+			 */
+			this.operations = [];
+		}
+
+		/**
+		 * Add operation to the delta.
+		 *
+		 * @param {document.operation.Operation} operation Operation instance.
+		 */
+		addOperation( operation ) {
+			operation.delta = this;
+			this.operations.push( operation );
+
+			return operation;
+		}
+	}
+
+	return Delta;
+} );

+ 44 - 0
packages/ckeditor5-engine/src/document/delta/insertdelta.js

@@ -0,0 +1,44 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [
+	'document/delta/delta',
+	'document/delta/register',
+	'document/operation/insertoperation'
+], ( Delta, register, InsertOperation ) => {
+	/**
+	 * To provide specific OT behavior and better collisions solving, the {@link document.Transaction#insert} method
+	 * uses the `InsertDelta` class which inherits from the `Delta` class and may overwrite some methods.
+	 *
+	 * @class document.delta.InsertDelta
+	 */
+	class InsertDelta extends Delta {}
+
+	/**
+	 * Inserts a node or nodes at the given position.
+	 *
+	 * @chainable
+	 * @memberOf document.Transaction
+	 * @method insert
+	 * @param {document.Position} position Position of insertion.
+	 * @param {document.Node|document.Text|document.NodeList|String|Iterable} nodes The list of nodes to be inserted.
+	 * List of nodes can be of any type accepted by the {@link document.NodeList} constructor.
+	 */
+	register( 'insert', function( position, nodes ) {
+		const delta = new InsertDelta();
+
+		const operation = new InsertOperation( position, nodes, this.doc.version );
+		this.doc.applyOperation( operation );
+		delta.addOperation( operation );
+
+		this.addDelta( delta );
+
+		return this;
+	} );
+
+	return InsertDelta;
+} );

+ 78 - 0
packages/ckeditor5-engine/src/document/delta/mergedelta.js

@@ -0,0 +1,78 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [
+	'document/delta/delta',
+	'document/delta/register',
+	'document/position',
+	'document/element',
+	'document/operation/removeoperation',
+	'document/operation/moveoperation',
+	'ckeditorerror'
+], ( Delta, register, Position, Element, RemoveOperation, MoveOperation, CKEditorError ) => {
+	/**
+	 * To provide specific OT behavior and better collisions solving, {@link document.Transaction#merge} method
+	 * uses the `MergeDelta` class which inherits from the `Delta` class and may overwrite some methods.
+	 *
+	 * @class document.delta.MergeDelta
+	 */
+	class MergeDelta extends Delta {}
+
+	/**
+	 * Merges two siblings at the given position.
+	 *
+	 * Node before and after the position have to be an element. Otherwise `transaction-merge-no-element-before` or
+	 * `transaction-merge-no-element-after` error will be thrown.
+	 *
+	 * @chainable
+	 * @method merge
+	 * @memberOf document.Transaction
+	 * @param {document.Position} position Position of merge.
+	 */
+	register( 'merge', function( position ) {
+		const delta = new MergeDelta();
+		const nodeBefore = position.nodeBefore;
+		const nodeAfter = position.nodeAfter;
+
+		if ( !( nodeBefore instanceof Element ) ) {
+			/**
+			 * Node before merge position must be an element.
+			 *
+			 * @error transaction-merge-no-element-before
+			 */
+			throw new CKEditorError(
+				'transaction-merge-no-element-before: Node before merge position must be an element.' );
+		}
+
+		if ( !( nodeAfter instanceof Element ) ) {
+			/**
+			 * Node after merge position must be an element.
+			 *
+			 * @error transaction-merge-no-element-after
+			 */
+			throw new CKEditorError(
+				'transaction-merge-no-element-after: Node after merge position must be an element.' );
+		}
+
+		const positionAfter = Position.createFromParentAndOffset( nodeAfter, 0 );
+		const positionBefore = Position.createFromParentAndOffset( nodeBefore, nodeBefore.getChildCount() );
+
+		const move = new MoveOperation( positionAfter, positionBefore, nodeAfter.getChildCount(), this.doc.version );
+		this.doc.applyOperation( move );
+		delta.addOperation( move );
+
+		const remove = new RemoveOperation( position, 1, this.doc.version );
+		this.doc.applyOperation( remove );
+		delta.addOperation( remove );
+
+		this.addDelta( delta );
+
+		return this;
+	} );
+
+	return MergeDelta;
+} );

+ 14 - 0
packages/ckeditor5-engine/src/document/delta/register.js

@@ -0,0 +1,14 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+// Register method exposed for deltas, which needs only this method, to make code simpler, more beautiful and, first of
+// all, to solve circular dependencies.
+CKEDITOR.define( [
+	'document/delta/transaction-base'
+], ( Transaction ) => {
+	return Transaction.register;
+} );

+ 47 - 0
packages/ckeditor5-engine/src/document/delta/removedelta.js

@@ -0,0 +1,47 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [
+	'document/delta/delta',
+	'document/delta/register',
+	'document/operation/removeoperation'
+], ( Delta, register, RemoveOperation ) => {
+	/**
+	 * To provide specific OT behavior and better collisions solving, {@link document.Transaction#remove} method
+	 * uses the `RemoveDelta` class which inherits from the `Delta` class and may overwrite some methods.
+	 *
+	 * @class document.delta.RemoveDelta
+	 */
+	class RemoveDelta extends Delta {}
+
+	/**
+	 * Removes nodes starting from the given position.
+	 *
+	 * @chainable
+	 * @method remove
+	 * @memberOf document.Transaction
+	 * @param {document.Position} position Position before the first node to remove.
+	 * @param {Number} howMany How many nodes to remove.
+	 */
+	register( 'remove', function( position, howMany ) {
+		if ( typeof howMany !== 'number' ) {
+			howMany = 1;
+		}
+
+		const delta = new RemoveDelta();
+
+		const operation = new RemoveOperation( position, howMany, this.doc.version );
+		this.doc.applyOperation( operation );
+		delta.addOperation( operation );
+
+		this.addDelta( delta );
+
+		return this;
+	} );
+
+	return RemoveDelta;
+} );

+ 71 - 0
packages/ckeditor5-engine/src/document/delta/splitdelta.js

@@ -0,0 +1,71 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [
+	'document/delta/delta',
+	'document/delta/register',
+	'document/position',
+	'document/element',
+	'document/operation/insertoperation',
+	'document/operation/moveoperation',
+	'ckeditorerror'
+], ( Delta, register, Position, Element, InsertOperation, MoveOperation, CKEditorError ) => {
+	/**
+	 * To provide specific OT behavior and better collisions solving, the {@link document.Transaction#split} method
+	 * uses `SplitDelta` class which inherits from the `Delta` class and may overwrite some methods.
+	 *
+	 * @class document.delta.SplitDelta
+	 */
+	class SplitDelta extends Delta {}
+
+	/**
+	 * Splits a node at the given position.
+	 *
+	 * This cannot be a position inside the root element. The `transaction-split-root` error will be thrown if
+	 * you try to split the root element.
+	 *
+	 * @chainable
+	 * @method split
+	 * @memberOf document.Transaction
+	 * @param {document.Position} position Position of split.
+	 */
+	register( 'split', function( position ) {
+		const delta = new SplitDelta();
+		const splitElement = position.parent;
+
+		if ( !splitElement.parent ) {
+			/**
+			 * Root element can not be split.
+			 *
+			 * @error transaction-split-root
+			 */
+			throw new CKEditorError( 'transaction-split-root: Root element can not be split.' );
+		}
+
+		const copy = new Element( splitElement.name, splitElement.getAttrs() );
+		const insert = new InsertOperation( Position.createAfter( splitElement ), copy, this.doc.version );
+
+		this.doc.applyOperation( insert );
+		delta.addOperation( insert );
+
+		const move = new MoveOperation(
+			position,
+			Position.createFromParentAndOffset( copy, 0 ),
+			splitElement.getChildCount() - position.offset,
+			this.doc.version
+		);
+
+		this.doc.applyOperation( move );
+		delta.addOperation( move );
+
+		this.addDelta( delta );
+
+		return this;
+	} );
+
+	return SplitDelta;
+} );

+ 124 - 0
packages/ckeditor5-engine/src/document/delta/transaction-base.js

@@ -0,0 +1,124 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [ 'ckeditorerror' ], ( CKEditorError ) => {
+	/**
+	 * The transaction class groups document changes (deltas). All deltas grouped in a single transactions can be
+	 * reverted together, so you can think about the transaction as a single undo step. If you want to extend one
+	 * undo step you can call another method on the same transaction object. If you want to create a separate undo step
+	 * you can create a new transaction.
+	 *
+	 * For example to create two separate undo steps you can call:
+	 *
+	 *		doc.createTransaction().insert( firstPosition, 'foo' );
+	 *		doc.createTransaction().insert( secondPosition, 'bar' );
+	 *
+	 * To create a single undo step:
+	 *
+	 *		const transaction = doc.createTransaction()
+	 *		transaction.insert( firstPosition, 'foo' );
+	 *		transaction.insert( secontPosition, 'bar' );
+	 *
+	 * Note that all document modification methods (insert, remove, split, etc.) are chainable so you can shorten code to:
+	 *
+	 *		doc.createTransaction().insert( firstPosition, 'foo' ).insert( secontPosition, 'bar' );
+	 *
+	 * @class document.Transaction
+	 */
+	class Transaction {
+		/**
+		 * Creates transaction instance. Not recommended to use directly, use {@link document.Document#createTransaction}
+		 * instead.
+		 *
+		 * @constructor
+		 * @param {document.Document} doc Document which this transaction changes.
+		 */
+		constructor( doc ) {
+			/**
+			 * Document which this transaction changes.
+			 *
+			 * @readonly
+			 * @type {document.Document}
+			 */
+			this.doc = doc;
+
+			/**
+			 * Array of deltas which compose transaction.
+			 *
+			 * @readonly
+			 * @type {document.delta.Delta[]}
+			 */
+			this.deltas = [];
+		}
+
+		/**
+		 * Adds delta to the transaction instance. All modification methods (insert, remove, split, etc.) use this method
+		 * to add created deltas.
+		 *
+		 * @param {document.delta.Delta} delta Delta to add.
+		 * @return {document.delta.Delta} Added delta.
+		 */
+		addDelta( delta ) {
+			delta.transaction = this;
+			this.deltas.push( delta );
+
+			return delta;
+		}
+
+		/**
+		 * Static method to register transaction methods. To make code scalable transaction do not have modification
+		 * methods built in. They can be registered using this method.
+		 *
+		 * This method checks if there is no naming collision and throw `transaction-register-taken` if the method name
+		 * is already taken.
+		 *
+		 * Beside that no magic happens here, the method is added to the `Transaction` class prototype.
+		 *
+		 * For example:
+		 *
+		 *		Transaction.register( 'insert', function( position, nodes ) {
+		 *			// You can use a class inherit from Delta if that class should handle OT in the special way.
+		 *			const delta = new Delta();
+		 *
+		 * 			// Create operations which should be components of this delta.
+		 *			const operation = new InsertOperation( position, nodes, this.doc.version );
+		 *
+		 *			// Remember to apply every operation, no magic, you need to do it manually.
+		 *			this.doc.applyOperation( operation );
+		 *
+		 *			// Add operation to the delta.
+		 *			delta.addOperation( operation );
+		 *
+		 *			// Add delta to the transaction instance.
+		 *			this.addDelta( delta );
+		 *
+		 * 			// Make this method chainable.
+		 * 			return this;
+		 *		} );
+		 *
+		 * @param {String} name Method name.
+		 * @param {Fuction} creator Method body.
+		 */
+		static register( name, creator ) {
+			if ( Transaction.prototype[ name ] ) {
+				/**
+				 * This transaction method is already taken.
+				 *
+				 * @error transaction-register-taken
+				 * @param {String} name
+				 */
+				throw new CKEditorError(
+					'transaction-register-taken: This transaction method is already taken.',
+					{ name: name } );
+			}
+
+			Transaction.prototype[ name ] = creator;
+		}
+	}
+
+	return Transaction;
+} );

+ 24 - 4
packages/ckeditor5-engine/src/document/document.js

@@ -8,20 +8,29 @@
 CKEDITOR.define( [
 	'document/element',
 	'document/rootelement',
+	'document/transaction',
 	'emittermixin',
 	'utils',
 	'ckeditorerror'
-], ( Element, RootElement, EmitterMixin, utils, CKEditorError ) => {
+], ( Element, RootElement, Tranaction, EmitterMixin, utils, CKEditorError ) => {
 	const graveyardSymbol = Symbol( 'graveyard' );
 
 	/**
-	 * Document model.
+	 * Document tree model describes all editable data in the editor. It may contain multiple {@link #roots root elements},
+	 * for example if the editor have multiple editable areas, each area will be represented by the separate root.
+	 *
+	 * All changes in the document are done by {@link document.operation.Operation operations}. To create operations in
+	 * the simple way use use the {@link document.Transaction transaction} API, for example:
+	 *
+	 *		document.createTransaction().insert( position, nodes ).split( otherPosition );
+	 *
+	 * @see #createTransaction
 	 *
 	 * @class document.Document
 	 */
 	class Document {
 		/**
-		 * Creates an empty document instance.
+		 * Creates an empty document instance with no {@link #roots}.
 		 *
 		 * @constructor
 		 */
@@ -49,7 +58,9 @@ CKEDITOR.define( [
 		}
 
 		/**
-		 * This is the only entry point for all document changes.
+		 * This is the entry point for all document changes. All changes on the document are done using
+		 * {@link document.operation.Operation operations}. To create operations in the simple way use the
+		 * {@link document.Transaction} API available via {@link #createTransaction} method.
 		 *
 		 * @param {document.operation.Operation} operation Operation to be applied.
 		 */
@@ -131,6 +142,15 @@ CKEDITOR.define( [
 		get _graveyard() {
 			return this.getRoot( graveyardSymbol );
 		}
+
+		/**
+		 * Creates a {@link document.Transaction} instance which allows to change the document.
+		 *
+		 * @returns {document.Transaction} Transaction instance.
+		 */
+		createTransaction() {
+			return new Tranaction( this );
+		}
 	}
 
 	utils.extend( Document.prototype, EmitterMixin );

+ 6 - 1
packages/ckeditor5-engine/src/document/element.js

@@ -5,7 +5,12 @@
 
 'use strict';
 
-CKEDITOR.define( [ 'document/node', 'document/nodelist' ], ( Node, NodeList ) => {
+CKEDITOR.define( [
+	'document/node',
+	'document/nodelist',
+	'document/range',
+	'document/position'
+], ( Node, NodeList, Range, Position ) => {
 	/**
 	 * Tree data model element.
 	 *

+ 6 - 11
packages/ckeditor5-engine/src/document/node.js

@@ -232,19 +232,14 @@ CKEDITOR.define( [ 'document/attribute', 'utils', 'ckeditorerror' ], ( Attribute
 		}
 
 		/**
-		 * Gets the number of attributes.
+		 * Returns attribute iterator. It can be use to create a new element with the same attributes:
 		 *
-		 * @protected
-		 * @returns {Number} Number of attributes.
+		 *		const copy = new Element( element.name, element.getAttrs() );
+		 *
+		 * @returns {Iterable.<document.Attribute>} Attribute iterator.
 		 */
-		_getAttrCount() {
-			let count = 0;
-
-			for ( let attr of this._attrs ) { // jshint ignore:line
-				count++;
-			}
-
-			return count;
+		getAttrs() {
+			return this._attrs[ Symbol.iterator ]();
 		}
 	}
 

+ 8 - 0
packages/ckeditor5-engine/src/document/operation/operation.js

@@ -30,6 +30,14 @@ CKEDITOR.define( [], () => {
 			 */
 			this.baseVersion = baseVersion;
 
+			/**
+			 * {@link Document.Delta Delta} which the operation is a part of. This property is set by the
+			 * {@link Document.Delta delta} when the operations is added to it by the
+			 * {@link Document.Delta#addOperation} method.
+			 *
+			 * @property {Document.Delta} delta
+			 */
+
 			/**
 			 * Executes the operation - modifications described by the operation attributes
 			 * will be applied to the tree model.

+ 3 - 0
packages/ckeditor5-engine/src/document/positioniterator.js

@@ -67,6 +67,9 @@ CKEDITOR.define( [
 			const position = this.position;
 			const parent = position.parent;
 
+			// Ugh... added here because of circular deps in AMD ;<.
+			Element = CKEDITOR.require( 'document/element' );
+
 			// We are at the end of the root.
 			if ( parent.parent === null && position.offset === parent.getChildCount() ) {
 				return { done: true };

+ 27 - 1
packages/ckeditor5-engine/src/document/range.js

@@ -5,7 +5,7 @@
 
 'use strict';
 
-CKEDITOR.define( [ 'document/positioniterator' ], ( PositionIterator ) => {
+CKEDITOR.define( [ 'document/positioniterator', 'document/position' ], ( PositionIterator, Position ) => {
 	/**
 	 * Range class. Range is iterable.
 	 *
@@ -35,6 +35,32 @@ CKEDITOR.define( [ 'document/positioniterator' ], ( PositionIterator ) => {
 			this.end = end;
 		}
 
+		/**
+		 * Creates a range inside an element which starts before the first child and ends after the last child.
+		 *
+		 * @param {document.Element} element Element which is a parent for the range.
+		 * @returns {document.Range} Created range.
+		 */
+		static createFromElement( element ) {
+			return Range.createFromParentsAndOffsets( element, 0, element, element.getChildCount() );
+		}
+
+		/**
+		 * Creates a range from given parents and offsets.
+		 *
+		 * @param {document.Element} startElement Start position parent element.
+		 * @param {Number} startOffset Start position offset.
+		 * @param {document.Element} endElement End position parent element.
+		 * @param {Number} endOffset End position offset.
+		 * @returns {document.Range} Created range.
+		 */
+		static createFromParentsAndOffsets( startElement, startOffset, endElement, endOffset ) {
+			return new Range(
+					Position.createFromParentAndOffset( startElement, startOffset ),
+					Position.createFromParentAndOffset( endElement, endOffset )
+				);
+		}
+
 		/**
 		 * Two ranges equal if their start and end positions equal.
 		 *

+ 21 - 0
packages/ckeditor5-engine/src/document/transaction.js

@@ -0,0 +1,21 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+// All deltas need to be loaded so they can register themselves as transaction methods.
+//
+// To solve circular dependencies (deltas need to require transaction class), transaction class body is moved
+// to document/delta/transaction-base.
+CKEDITOR.define( [
+	'document/delta/transaction-base',
+	'document/delta/insertdelta',
+	'document/delta/removedelta',
+	'document/delta/changedelta',
+	'document/delta/splitdelta',
+	'document/delta/mergedelta'
+], ( Transaction ) => {
+	return Transaction;
+} );

+ 18 - 0
packages/ckeditor5-engine/tests/_tools/tools.js

@@ -47,6 +47,24 @@
 
 				return TestCreator;
 			}
+		},
+
+		/**
+		 * Returns the number of elements return by the iterator.
+		 *
+		 *	  bender.tools.core.getIteratorCount( [ 1, 2, 3, 4, 5 ] ); // 5;
+		 *
+		 * @param {Iterable.<*>} iterator Any iterator.
+		 * @returns {Number} Number of elements returned by that iterator.
+		 */
+		getIteratorCount: ( iterator ) => {
+			let count = 0;
+
+			for ( let _ of iterator ) { // jshint ignore:line
+				count++;
+			}
+
+			return count;
 		}
 	};
 } )();

+ 7 - 0
packages/ckeditor5-engine/tests/bender/tools.js

@@ -57,4 +57,11 @@ describe( 'bender.tools.core.defineEditorCreatorMock()', () => {
 		expect( TestCreator3.prototype ).to.have.property( 'create', createFn3 );
 		expect( TestCreator3.prototype ).to.have.property( 'destroy', destroyFn3 );
 	} );
+} );
+
+describe( 'bender.tools.core.getIteratorCount()', () => {
+	it( 'should returns number of editable items ', () => {
+		const count = bender.tools.core.getIteratorCount( [ 1, 2, 3, 4, 5 ] );
+		expect( count ).to.equal( 5 );
+	} );
 } );

+ 6 - 2
packages/ckeditor5-engine/tests/document/character.js

@@ -7,8 +7,12 @@
 
 /* bender-tags: document */
 
+/* bender-include: ../_tools/tools.js */
+
 'use strict';
 
+const getIteratorCount = bender.tools.core.getIteratorCount;
+
 const modules = bender.amd.require(
 	'document/character',
 	'document/node',
@@ -34,7 +38,7 @@ describe( 'Character', () => {
 			expect( character ).to.be.an.instanceof( Node );
 			expect( character ).to.have.property( 'character' ).that.equals( 'f' );
 			expect( character ).to.have.property( 'parent' ).that.equals( parent );
-			expect( character._getAttrCount() ).to.equal( 0 );
+			expect( getIteratorCount( character.getAttrs() ) ).to.equal( 0 );
 		} );
 
 		it( 'should create character with attributes', () => {
@@ -45,7 +49,7 @@ describe( 'Character', () => {
 			expect( character ).to.be.an.instanceof( Node );
 			expect( character ).to.have.property( 'character' ).that.equals( 'f' );
 			expect( character ).to.have.property( 'parent' ).that.equals( parent );
-			expect( character._getAttrCount() ).to.equal( 1 );
+			expect( getIteratorCount( character.getAttrs() ) ).to.equal( 1 );
 			expect( character.getAttr( attr.key ) ).to.equal( attr.value );
 		} );
 	} );

+ 275 - 0
packages/ckeditor5-engine/tests/document/deltas/changedelta.js

@@ -0,0 +1,275 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: document, delta */
+
+/* bender-include: ../../_tools/tools.js */
+
+'use strict';
+
+const getIteratorCount = bender.tools.core.getIteratorCount;
+
+const modules = bender.amd.require(
+	'document/transaction',
+	'document/document',
+	'document/text',
+	'document/attribute',
+	'document/range',
+	'document/position',
+	'document/element',
+	'document/character' );
+
+describe( 'Transaction', () => {
+	let Transaction, Document, Text, Attribute, Range, Position, Element, Character;
+
+	let doc, root, transaction;
+
+	before( () => {
+		Transaction = modules[ 'document/transaction' ];
+		Document = modules[ 'document/document' ];
+		Text = modules[ 'document/text' ];
+		Attribute = modules[ 'document/attribute' ];
+		Range = modules[ 'document/range' ];
+		Position = modules[ 'document/position' ];
+		Element = modules[ 'document/element' ];
+		Character = modules[ 'document/character' ];
+	} );
+
+	beforeEach( () => {
+		doc = new Document();
+		root = doc.createRoot( 'root' );
+		transaction = doc.createTransaction();
+	} );
+
+	function getOperationsCount() {
+		let count = 0;
+
+		for ( let delta of transaction.deltas ) {
+			count += getIteratorCount( delta.operations );
+		}
+
+		return count;
+	}
+
+	describe( 'change attribute on node', () => {
+		let node, character;
+
+		beforeEach( () => {
+			node = new Element( 'p', [ new Attribute( 'a', 1 ) ] );
+			character = new Character( 'c', [ new Attribute( 'a', 1 ) ] );
+			root.insertChildren( 0, [ node, character ] );
+		} );
+
+		describe( 'setAttr', () => {
+			it( 'should create the attribute on element', () => {
+				transaction.setAttr( 'b', 2, node );
+				expect( getOperationsCount() ).to.equal( 1 );
+				expect( node.getAttr( 'b' ) ).to.equal( 2 );
+			} );
+
+			it( 'should change the attribute of element', () => {
+				transaction.setAttr( 'a', 2, node );
+				expect( getOperationsCount() ).to.equal( 1 );
+				expect( node.getAttr( 'a' ) ).to.equal( 2 );
+			} );
+
+			it( 'should create the attribute on character', () => {
+				transaction.setAttr( 'b', 2, character );
+				expect( getOperationsCount() ).to.equal( 1 );
+				expect( character.getAttr( 'b' ) ).to.equal( 2 );
+			} );
+
+			it( 'should change the attribute of character', () => {
+				transaction.setAttr( 'a', 2, character );
+				expect( getOperationsCount() ).to.equal( 1 );
+				expect( character.getAttr( 'a' ) ).to.equal( 2 );
+			} );
+
+			it( 'should do nothing if the attribute value is the same', () => {
+				transaction.setAttr( 'a', 1, node );
+				expect( getOperationsCount() ).to.equal( 0 );
+				expect( node.getAttr( 'a' ) ).to.equal( 1 );
+			} );
+
+			it( 'should be chainable', () => {
+				const chain = transaction.setAttr( 'b', 2, node );
+				expect( chain ).to.equal( transaction );
+			} );
+		} );
+
+		describe( 'removeAttr', () => {
+			it( 'should remove the attribute from element', () => {
+				transaction.removeAttr( 'a', node );
+				expect( getOperationsCount() ).to.equal( 1 );
+				expect( node.getAttr( 'a' ) ).to.be.null;
+			} );
+
+			it( 'should remove the attribute from character', () => {
+				transaction.removeAttr( 'a', character );
+				expect( getOperationsCount() ).to.equal( 1 );
+				expect( character.getAttr( 'a' ) ).to.be.null;
+			} );
+
+			it( 'should do nothing if the attribute is not set', () => {
+				transaction.removeAttr( 'b', node );
+				expect( getOperationsCount() ).to.equal( 0 );
+			} );
+
+			it( 'should be chainable', () => {
+				const chain = transaction.removeAttr( 'a', node );
+				expect( chain ).to.equal( transaction );
+			} );
+		} );
+	} );
+
+	describe( 'change attribute on range', () => {
+		beforeEach( () => {
+			root.insertChildren( 0, [
+				new Text( 'xxx', [ new Attribute( 'a', 1 ) ] ),
+				'xxx',
+				new Text( 'xxx', [ new Attribute( 'a', 1 ) ] ),
+				new Text( 'xxx', [ new Attribute( 'a', 2 ) ] ),
+				'xxx',
+				new Text( 'xxx', [ new Attribute( 'a', 1 ) ] )
+			] );
+		} );
+
+		function getRange( startIndex, endIndex ) {
+			return new Range(
+					Position.createFromParentAndOffset( root, startIndex ),
+					Position.createFromParentAndOffset( root, endIndex )
+				);
+		}
+
+		function getChangesAttrsCount() {
+			let count = 0;
+
+			for ( let delta of transaction.deltas ) {
+				for ( let operation of delta.operations ) {
+					count += getIteratorCount( operation.range );
+				}
+			}
+
+			return count;
+		}
+
+		function getCompressedAttrs() {
+			// default: 111---111222---111
+			const range = Range.createFromElement( root );
+
+			return Array.from( range ).map( value => value.node.getAttr( 'a' ) || '-' ).join( '' );
+		}
+
+		describe( 'setAttr', () => {
+			it( 'should set the attribute on the range', () => {
+				transaction.setAttr( 'a', 3, getRange( 3, 6 ) );
+				expect( getOperationsCount() ).to.equal( 1 );
+				expect( getChangesAttrsCount() ).to.equal( 3 );
+				expect( getCompressedAttrs() ).to.equal( '111333111222---111' );
+			} );
+
+			it( 'should split the operations if parts of the range have different attributes', () => {
+				transaction.setAttr( 'a', 3, getRange( 4, 14 ) );
+				expect( getOperationsCount() ).to.equal( 4 );
+				expect( getChangesAttrsCount() ).to.equal( 10 );
+				expect( getCompressedAttrs() ).to.equal( '111-3333333333-111' );
+			} );
+
+			it( 'should split the operations if parts of the part of the range have the attribute', () => {
+				transaction.setAttr( 'a', 2, getRange( 4, 14 ) );
+				expect( getOperationsCount() ).to.equal( 3 );
+				expect( getChangesAttrsCount() ).to.equal( 7 );
+				expect( getCompressedAttrs() ).to.equal( '111-2222222222-111' );
+			} );
+
+			it( 'should strip the range if the beginning have the attribute', () => {
+				transaction.setAttr( 'a', 1, getRange( 1, 5 ) );
+				expect( getOperationsCount() ).to.equal( 1 );
+				expect( getChangesAttrsCount() ).to.equal( 2 );
+				expect( getCompressedAttrs() ).to.equal( '11111-111222---111' );
+			} );
+
+			it( 'should strip the range if the ending have the attribute', () => {
+				transaction.setAttr( 'a', 1, getRange( 13, 17 ) );
+				expect( getOperationsCount() ).to.equal( 1 );
+				expect( getChangesAttrsCount() ).to.equal( 2 );
+				expect( getCompressedAttrs() ).to.equal( '111---111222-11111' );
+			} );
+
+			it( 'should do nothing if the range has attribute', () => {
+				transaction.setAttr( 'a', 1, getRange( 0, 3 ) );
+				expect( getOperationsCount() ).to.equal( 0 );
+				expect( getCompressedAttrs() ).to.equal( '111---111222---111' );
+			} );
+
+			it( 'should create a proper operations for the mixed range', () => {
+				transaction.setAttr( 'a', 1, getRange( 0, 18 ) );
+				expect( getOperationsCount() ).to.equal( 3 );
+				expect( getChangesAttrsCount() ).to.equal( 9 );
+				expect( getCompressedAttrs() ).to.equal( '111111111111111111' );
+			} );
+
+			it( 'should be chainable', () => {
+				const chain = transaction.setAttr( 'a', 3, getRange( 3, 6 ) );
+				expect( chain ).to.equal( transaction );
+			} );
+		} );
+
+		describe( 'removeAttr', () => {
+			it( 'should remove the attribute on the range', () => {
+				transaction.removeAttr( 'a', getRange( 0, 2 ) );
+				expect( getOperationsCount() ).to.equal( 1 );
+				expect( getChangesAttrsCount() ).to.equal( 2 );
+				expect( getCompressedAttrs() ).to.equal( '--1---111222---111' );
+			} );
+
+			it( 'should split the operations if parts of the range have different attributes', () => {
+				transaction.removeAttr( 'a', getRange( 7, 11 ) );
+				expect( getOperationsCount() ).to.equal( 2 );
+				expect( getChangesAttrsCount() ).to.equal( 4 );
+				expect( getCompressedAttrs() ).to.equal( '111---1----2---111' );
+			} );
+
+			it( 'should split the operations if parts of the part of the range have no attribute', () => {
+				transaction.removeAttr( 'a', getRange( 1, 7 ) );
+				expect( getOperationsCount() ).to.equal( 2 );
+				expect( getChangesAttrsCount() ).to.equal( 3 );
+				expect( getCompressedAttrs() ).to.equal( '1------11222---111' );
+			} );
+
+			it( 'should strip the range if the beginning have no attribute', () => {
+				transaction.removeAttr( 'a', getRange( 4, 12 ) );
+				expect( getOperationsCount() ).to.equal( 2 );
+				expect( getChangesAttrsCount() ).to.equal( 6 );
+				expect( getCompressedAttrs() ).to.equal( '111------------111' );
+			} );
+
+			it( 'should strip the range if the ending have no attribute', () => {
+				transaction.removeAttr( 'a', getRange( 7, 15 ) );
+				expect( getOperationsCount() ).to.equal( 2 );
+				expect( getChangesAttrsCount() ).to.equal( 5 );
+				expect( getCompressedAttrs() ).to.equal( '111---1--------111' );
+			} );
+
+			it( 'should do nothing if the range has no attribute', () => {
+				transaction.removeAttr( 'a', getRange( 4, 5 ) );
+				expect( getOperationsCount() ).to.equal( 0 );
+				expect( getCompressedAttrs() ).to.equal( '111---111222---111' );
+			} );
+
+			it( 'should create a proper operations for the mixed range', () => {
+				transaction.removeAttr( 'a', getRange( 3, 15 ) );
+				expect( getOperationsCount() ).to.equal( 2 );
+				expect( getChangesAttrsCount() ).to.equal( 6 );
+				expect( getCompressedAttrs() ).to.equal( '111------------111' );
+			} );
+
+			it( 'should be chainable', () => {
+				const chain = transaction.removeAttr( 'a', getRange( 0, 2 ) );
+				expect( chain ).to.equal( transaction );
+			} );
+		} );
+	} );
+} );

+ 67 - 0
packages/ckeditor5-engine/tests/document/deltas/delta.js

@@ -0,0 +1,67 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: document, delta */
+
+/* bender-include: ../../_tools/tools.js */
+
+'use strict';
+
+const getIteratorCount = bender.tools.core.getIteratorCount;
+
+const modules = bender.amd.require(
+	'document/delta/delta' );
+
+describe( 'Delta', () => {
+	let Delta;
+
+	before( () => {
+		Delta = modules[ 'document/delta/delta' ];
+	} );
+
+	describe( 'constructor', () => {
+		it( 'should create an delta with empty properties', () => {
+			const delta = new Delta();
+
+			expect( delta ).to.have.property( 'transaction' ).that.is.null;
+			expect( delta ).to.have.property( 'operations' ).that.a( 'array' ).and.have.length( 0 );
+		} );
+	} );
+
+	describe( 'addOperation', () => {
+		it( 'should add operation to the delta', () => {
+			const delta = new Delta();
+			const operation = {};
+
+			delta.addOperation( operation );
+
+			expect( delta.operations ).to.have.length( 1 );
+			expect( delta.operations[ 0 ] ).to.equal( operation );
+		} );
+
+		it( 'should add delta property to the operation', () => {
+			const delta = new Delta();
+			const operation = {};
+
+			delta.addOperation( operation );
+
+			expect( operation.delta ).to.equal( delta );
+		} );
+	} );
+
+	describe( 'iterator', () => {
+		it( 'should iterate over delta operations', () => {
+			const delta = new Delta();
+
+			delta.addOperation( {} );
+			delta.addOperation( {} );
+			delta.addOperation( {} );
+
+			const count = getIteratorCount( delta.operations );
+
+			expect( count ).to.equal( 3 );
+		} );
+	} );
+} );

+ 46 - 0
packages/ckeditor5-engine/tests/document/deltas/insertdelta.js

@@ -0,0 +1,46 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: document, delta */
+
+'use strict';
+
+const modules = bender.amd.require(
+	'document/document',
+	'document/position' );
+
+describe( 'Transaction', () => {
+	let Document, Position;
+
+	let doc, root;
+
+	before( () => {
+		Document = modules[ 'document/document' ];
+		Position = modules[ 'document/position' ];
+	} );
+
+	beforeEach( () => {
+		doc = new Document();
+		root = doc.createRoot( 'root' );
+	} );
+
+	it( 'should insert text', () => {
+		const position = new Position( [ 0 ], root );
+		doc.createTransaction().insert( position, 'foo' );
+
+		expect( root.getChildCount() ).to.equal( 3 );
+		expect( root.getChild( 0 ).character ).to.equal( 'f' );
+		expect( root.getChild( 1 ).character ).to.equal( 'o' );
+		expect( root.getChild( 2 ).character ).to.equal( 'o' );
+	} );
+
+	it( 'should be chainable', () => {
+		const position = new Position( [ 0 ], root );
+		const transaction = doc.createTransaction();
+
+		const chain = transaction.insert( position, 'foo' );
+		expect( chain ).to.equal( transaction );
+	} );
+} );

+ 80 - 0
packages/ckeditor5-engine/tests/document/deltas/mergedelta.js

@@ -0,0 +1,80 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: document, delta */
+
+/* bender-include: ../../_tools/tools.js */
+
+'use strict';
+
+const getIteratorCount = bender.tools.core.getIteratorCount;
+
+const modules = bender.amd.require(
+	'document/document',
+	'document/position',
+	'document/element',
+	'document/attribute',
+	'ckeditorerror' );
+
+describe( 'Transaction', () => {
+	let Document, Position, Element, Attribute, CKEditorError;
+
+	let doc, root, p1, p2;
+
+	before( () => {
+		Document = modules[ 'document/document' ];
+		Position = modules[ 'document/position' ];
+		Element = modules[ 'document/element' ];
+		Attribute = modules[ 'document/attribute' ];
+		CKEditorError = modules.ckeditorerror;
+	} );
+
+	beforeEach( () => {
+		doc = new Document();
+		root = doc.createRoot( 'root' );
+
+		p1 = new Element( 'p', [ new Attribute( 'key1', 'value1' ) ], 'foo' );
+		p2 = new Element( 'p', [ new Attribute( 'key2', 'value2' ) ], 'bar' );
+
+		root.insertChildren( 0, [ p1, p2 ] );
+	} );
+
+	describe( 'merge', () => {
+		it( 'should merge foo and bar into foobar', () => {
+			doc.createTransaction().merge( new Position( [ 1 ], root ) );
+
+			expect( root.getChildCount() ).to.equal( 1 );
+			expect( root.getChild( 0 ).name ).to.equal( 'p' );
+			expect( root.getChild( 0 ).getChildCount() ).to.equal( 6 );
+			expect( getIteratorCount( root.getChild( 0 ).getAttrs() ) ).to.equal( 1 );
+			expect( root.getChild( 0 ).getAttr( 'key1' ) ).to.equal( 'value1' );
+			expect( root.getChild( 0 ).getChild( 0 ).character ).to.equal( 'f' );
+			expect( root.getChild( 0 ).getChild( 1 ).character ).to.equal( 'o' );
+			expect( root.getChild( 0 ).getChild( 2 ).character ).to.equal( 'o' );
+			expect( root.getChild( 0 ).getChild( 3 ).character ).to.equal( 'b' );
+			expect( root.getChild( 0 ).getChild( 4 ).character ).to.equal( 'a' );
+			expect( root.getChild( 0 ).getChild( 5 ).character ).to.equal( 'r' );
+		} );
+
+		it( 'should throw if there is no element after', () => {
+			expect( () => {
+				doc.createTransaction().merge( new Position( [ 2 ], root ) );
+			} ).to.throw( CKEditorError, /^transaction-merge-no-element-after/ );
+		} );
+
+		it( 'should throw if there is no element before', () => {
+			expect( () => {
+				doc.createTransaction().merge( new Position( [ 0, 2 ], root ) );
+			} ).to.throw( CKEditorError, /^transaction-merge-no-element-before/ );
+		} );
+
+		it( 'should be chainable', () => {
+			const transaction = doc.createTransaction();
+
+			const chain = transaction.merge( new Position( [ 1 ], root ) );
+			expect( chain ).to.equal( transaction );
+		} );
+	} );
+} );

+ 61 - 0
packages/ckeditor5-engine/tests/document/deltas/removedelta.js

@@ -0,0 +1,61 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: document, delta */
+
+'use strict';
+
+const modules = bender.amd.require(
+	'document/document',
+	'document/position' );
+
+describe( 'Transaction', () => {
+	let Document, Position;
+
+	let doc, root;
+
+	before( () => {
+		Document = modules[ 'document/document' ];
+		Position = modules[ 'document/position' ];
+	} );
+
+	beforeEach( () => {
+		doc = new Document();
+		root = doc.createRoot( 'root' );
+		root.insertChildren( 0, 'foobar' );
+	} );
+
+	describe( 'remove', () => {
+		it( 'should remove one element', () => {
+			const position = new Position( [ 1 ], root );
+			doc.createTransaction().remove( position );
+
+			expect( root.getChildCount() ).to.equal( 5 );
+			expect( root.getChild( 0 ).character ).to.equal( 'f' );
+			expect( root.getChild( 1 ).character ).to.equal( 'o' );
+			expect( root.getChild( 2 ).character ).to.equal( 'b' );
+			expect( root.getChild( 3 ).character ).to.equal( 'a' );
+			expect( root.getChild( 4 ).character ).to.equal( 'r' );
+		} );
+
+		it( 'should remove 3 elements', () => {
+			const position = new Position( [ 1 ], root );
+			doc.createTransaction().remove( position, 3 );
+
+			expect( root.getChildCount() ).to.equal( 3 );
+			expect( root.getChild( 0 ).character ).to.equal( 'f' );
+			expect( root.getChild( 1 ).character ).to.equal( 'a' );
+			expect( root.getChild( 2 ).character ).to.equal( 'r' );
+		} );
+
+		it( 'should be chainable', () => {
+			const position = new Position( [ 1 ], root );
+			const transaction = doc.createTransaction();
+
+			const chain = transaction.remove( position );
+			expect( chain ).to.equal( transaction );
+		} );
+	} );
+} );

+ 101 - 0
packages/ckeditor5-engine/tests/document/deltas/splitdelta.js

@@ -0,0 +1,101 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: document, delta */
+
+/* bender-include: ../../_tools/tools.js */
+
+'use strict';
+
+const getIteratorCount = bender.tools.core.getIteratorCount;
+
+const modules = bender.amd.require(
+	'document/document',
+	'document/position',
+	'document/element',
+	'document/attribute',
+	'ckeditorerror' );
+
+describe( 'Transaction', () => {
+	let Document, Position, Element, Attribute, CKEditorError;
+
+	let doc, root, p;
+
+	before( () => {
+		Document = modules[ 'document/document' ];
+		Position = modules[ 'document/position' ];
+		Element = modules[ 'document/element' ];
+		Attribute = modules[ 'document/attribute' ];
+		CKEditorError = modules.ckeditorerror;
+	} );
+
+	beforeEach( () => {
+		doc = new Document();
+		root = doc.createRoot( 'root' );
+
+		p = new Element( 'p', [ new Attribute( 'key', 'value' ) ], 'foobar' );
+
+		root.insertChildren( 0, p );
+	} );
+
+	describe( 'split', () => {
+		it( 'should split foobar to foo and bar', () => {
+			doc.createTransaction().split( new Position( [ 0, 3 ], root ) );
+
+			expect( root.getChildCount() ).to.equal( 2 );
+
+			expect( root.getChild( 0 ).name ).to.equal( 'p' );
+			expect( root.getChild( 0 ).getChildCount() ).to.equal( 3 );
+			expect( getIteratorCount( root.getChild( 0 ).getAttrs() ) ).to.equal( 1 );
+			expect( root.getChild( 0 ).getAttr( 'key' ) ).to.equal( 'value' );
+			expect( root.getChild( 0 ).getChild( 0 ).character ).to.equal( 'f' );
+			expect( root.getChild( 0 ).getChild( 1 ).character ).to.equal( 'o' );
+			expect( root.getChild( 0 ).getChild( 2 ).character ).to.equal( 'o' );
+
+			expect( root.getChild( 1 ).name ).to.equal( 'p' );
+			expect( root.getChild( 1 ).getChildCount() ).to.equal( 3 );
+			expect( getIteratorCount( root.getChild( 1 ).getAttrs() ) ).to.equal( 1 );
+			expect( root.getChild( 1 ).getAttr( 'key' ) ).to.equal( 'value' );
+			expect( root.getChild( 1 ).getChild( 0 ).character ).to.equal( 'b' );
+			expect( root.getChild( 1 ).getChild( 1 ).character ).to.equal( 'a' );
+			expect( root.getChild( 1 ).getChild( 2 ).character ).to.equal( 'r' );
+		} );
+
+		it( 'should create an empty paragraph if we split at the end', () => {
+			doc.createTransaction().split( new Position( [ 0, 6 ], root ) );
+
+			expect( root.getChildCount() ).to.equal( 2 );
+
+			expect( root.getChild( 0 ).name ).to.equal( 'p' );
+			expect( root.getChild( 0 ).getChildCount() ).to.equal( 6 );
+			expect( getIteratorCount( root.getChild( 0 ).getAttrs() ) ).to.equal( 1 );
+			expect( root.getChild( 0 ).getAttr( 'key' ) ).to.equal( 'value' );
+			expect( root.getChild( 0 ).getChild( 0 ).character ).to.equal( 'f' );
+			expect( root.getChild( 0 ).getChild( 1 ).character ).to.equal( 'o' );
+			expect( root.getChild( 0 ).getChild( 2 ).character ).to.equal( 'o' );
+			expect( root.getChild( 0 ).getChild( 3 ).character ).to.equal( 'b' );
+			expect( root.getChild( 0 ).getChild( 4 ).character ).to.equal( 'a' );
+			expect( root.getChild( 0 ).getChild( 5 ).character ).to.equal( 'r' );
+
+			expect( root.getChild( 1 ).name ).to.equal( 'p' );
+			expect( root.getChild( 1 ).getChildCount() ).to.equal( 0 );
+			expect( getIteratorCount( root.getChild( 1 ).getAttrs() ) ).to.equal( 1 );
+			expect( root.getChild( 1 ).getAttr( 'key' ) ).to.equal( 'value' );
+		} );
+
+		it( 'should throw if we try to split a root', () => {
+			expect( () => {
+				doc.createTransaction().split( new Position( [ 0 ], root ) );
+			} ).to.throw( CKEditorError, /^transaction-split-root/ );
+		} );
+
+		it( 'should be chainable', () => {
+			const transaction = doc.createTransaction();
+
+			const chain = transaction.split( new Position( [ 0, 3 ], root ) );
+			expect( chain ).to.equal( transaction );
+		} );
+	} );
+} );

+ 12 - 1
packages/ckeditor5-engine/tests/document/document.js

@@ -10,15 +10,17 @@
 const modules = bender.amd.require(
 	'document/document',
 	'document/rootelement',
+	'document/transaction',
 	'ckeditorerror'
 );
 
 describe( 'Document', () => {
-	let Document, RootElement, CKEditorError;
+	let Document, RootElement, Transaction, CKEditorError;
 
 	before( () => {
 		Document = modules[ 'document/document' ];
 		RootElement = modules[ 'document/rootelement' ];
+		Transaction = modules[ 'document/transaction' ];
 		CKEditorError = modules.ckeditorerror;
 	} );
 
@@ -106,4 +108,13 @@ describe( 'Document', () => {
 			).to.throw( CKEditorError, /document-applyOperation-wrong-version/ );
 		} );
 	} );
+
+	describe( 'createTransaction', () => {
+		it( 'should create a new transaction with the document property', () => {
+			const transaction = document.createTransaction();
+
+			expect( transaction ).to.be.instanceof( Transaction );
+			expect( transaction ).to.have.property( 'doc' ).that.equals( document );
+		} );
+	} );
 } );

+ 6 - 2
packages/ckeditor5-engine/tests/document/element.js

@@ -7,8 +7,12 @@
 
 /* bender-tags: document */
 
+/* bender-include: ../_tools/tools.js */
+
 'use strict';
 
+const getIteratorCount = bender.tools.core.getIteratorCount;
+
 const modules = bender.amd.require(
 	'document/node',
 	'document/nodelist',
@@ -34,7 +38,7 @@ describe( 'Element', () => {
 			expect( element ).to.be.an.instanceof( Node );
 			expect( element ).to.have.property( 'name' ).that.equals( 'elem' );
 			expect( element ).to.have.property( 'parent' ).that.equals( parent );
-			expect( element._getAttrCount() ).to.equal( 0 );
+			expect( getIteratorCount( element.getAttrs() ) ).to.equal( 0 );
 		} );
 
 		it( 'should create element with attributes', () => {
@@ -46,7 +50,7 @@ describe( 'Element', () => {
 			expect( element ).to.be.an.instanceof( Node );
 			expect( element ).to.have.property( 'name' ).that.equals( 'elem' );
 			expect( element ).to.have.property( 'parent' ).that.equals( parent );
-			expect( element._getAttrCount() ).to.equal( 1 );
+			expect( getIteratorCount( element.getAttrs() ) ).to.equal( 1 );
 			expect( element.getAttr( attr.key ) ).to.equal( attr.value );
 		} );
 

+ 28 - 5
packages/ckeditor5-engine/tests/document/node.js

@@ -5,8 +5,12 @@
 
 /* bender-tags: document */
 
+/* bender-include: ../_tools/tools.js */
+
 'use strict';
 
+const getIteratorCount = bender.tools.core.getIteratorCount;
+
 const modules = bender.amd.require(
 	'document/element',
 	'document/character',
@@ -103,8 +107,8 @@ describe( 'Node', () => {
 
 			foo.removeAttr( 'attr' );
 
-			expect( foo._getAttrCount() ).to.equal( 0 );
-			expect( bar._getAttrCount() ).to.equal( 1 );
+			expect( getIteratorCount( foo.getAttrs() ) ).to.equal( 0 );
+			expect( getIteratorCount( bar.getAttrs() ) ).to.equal( 1 );
 		} );
 	} );
 
@@ -132,7 +136,7 @@ describe( 'Node', () => {
 
 			element.setAttr( attr );
 
-			expect( element._getAttrCount() ).to.equal( 1 );
+			expect( getIteratorCount( element.getAttrs() ) ).to.equal( 1 );
 			expect( element.getAttr( attr.key ) ).to.equal( attr.value );
 		} );
 
@@ -143,7 +147,7 @@ describe( 'Node', () => {
 
 			element.setAttr( newAttr );
 
-			expect( element._getAttrCount() ).to.equal( 1 );
+			expect( getIteratorCount( element.getAttrs() ) ).to.equal( 1 );
 			expect( element.getAttr( newAttr.key ) ).to.equal( newAttr.value );
 		} );
 	} );
@@ -157,7 +161,7 @@ describe( 'Node', () => {
 
 			element.removeAttr( attrB.key );
 
-			expect( element._getAttrCount() ).to.equal( 2 );
+			expect( getIteratorCount( element.getAttrs() ) ).to.equal( 2 );
 			expect( element.getAttr( attrA.key ) ).to.equal( attrA.value );
 			expect( element.getAttr( attrC.key ) ).to.equal( attrC.value );
 			expect( element.getAttr( attrB.key ) ).to.be.null;
@@ -206,6 +210,25 @@ describe( 'Node', () => {
 		} );
 	} );
 
+	describe( 'getAttrs', () => {
+		it( 'should allows to get attribute count', () => {
+			let element = new Element( 'foo', [
+				new Attribute( 1, true ),
+				new Attribute( 2, true ),
+				new Attribute( 3, true )
+			] );
+
+			expect( getIteratorCount( element.getAttrs() ) ).to.equal( 3 );
+		} );
+
+		it( 'should allows to copy attributes', () => {
+			let element = new Element( 'foo', [ new Attribute( 'x', true ) ] );
+			let copy = new Element( 'bar', element.getAttrs() );
+
+			expect( copy.getAttr( 'x' ) ).to.be.true;
+		} );
+	} );
+
 	describe( 'getIndex', () => {
 		it( 'should return null if the parent is null', () => {
 			expect( root.getIndex() ).to.be.null;

+ 20 - 16
packages/ckeditor5-engine/tests/document/operation/changeoperation.js

@@ -5,8 +5,12 @@
 
 /* bender-tags: document */
 
+/* bender-include: ../../_tools/tools.js */
+
 'use strict';
 
+const getIteratorCount = bender.tools.core.getIteratorCount;
+
 const modules = bender.amd.require(
 	'document/document',
 	'document/operation/changeoperation',
@@ -59,7 +63,7 @@ describe( 'ChangeOperation', () => {
 		expect( root.getChildCount() ).to.equal( 3 );
 		expect( root.getChild( 0 ).hasAttr( newAttr ) ).to.be.true;
 		expect( root.getChild( 1 ).hasAttr( newAttr ) ).to.be.true;
-		expect( root.getChild( 2 )._getAttrCount() ).to.equal( 0 );
+		expect( getIteratorCount( root.getChild( 2 ).getAttrs() ) ).to.equal( 0 );
 	} );
 
 	it( 'should add attribute to the existing attributes', () => {
@@ -80,7 +84,7 @@ describe( 'ChangeOperation', () => {
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.getChildCount() ).to.equal( 1 );
-		expect( root.getChild( 0 )._getAttrCount() ).to.equal( 3 );
+		expect( getIteratorCount( root.getChild( 0 ).getAttrs() ) ).to.equal( 3 );
 		expect( root.getChild( 0 ).hasAttr( newAttr ) ).to.be.true;
 		expect( root.getChild( 0 ).hasAttr( fooAttr ) ).to.be.true;
 		expect( root.getChild( 0 ).hasAttr( barAttr ) ).to.be.true;
@@ -103,11 +107,11 @@ describe( 'ChangeOperation', () => {
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.getChildCount() ).to.equal( 3 );
-		expect( root.getChild( 0 )._getAttrCount() ).to.equal( 1 );
+		expect( getIteratorCount( root.getChild( 0 ).getAttrs() ) ).to.equal( 1 );
 		expect( root.getChild( 0 ).hasAttr( newAttr ) ).to.be.true;
-		expect( root.getChild( 1 )._getAttrCount() ).to.equal( 1 );
+		expect( getIteratorCount( root.getChild( 1 ).getAttrs() ) ).to.equal( 1 );
 		expect( root.getChild( 1 ).hasAttr( newAttr ) ).to.be.true;
-		expect( root.getChild( 2 )._getAttrCount() ).to.equal( 1 );
+		expect( getIteratorCount( root.getChild( 2 ).getAttrs() ) ).to.equal( 1 );
 		expect( root.getChild( 2 ).hasAttr( oldAttr ) ).to.be.true;
 	} );
 
@@ -130,7 +134,7 @@ describe( 'ChangeOperation', () => {
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.getChildCount() ).to.equal( 1 );
-		expect( root.getChild( 0 )._getAttrCount() ).to.equal( 3 );
+		expect( getIteratorCount( root.getChild( 0 ).getAttrs() ) ).to.equal( 3 );
 		expect( root.getChild( 0 ).hasAttr( fooAttr ) ).to.be.true;
 		expect( root.getChild( 0 ).hasAttr( x2Attr ) ).to.be.true;
 		expect( root.getChild( 0 ).hasAttr( barAttr ) ).to.be.true;
@@ -154,7 +158,7 @@ describe( 'ChangeOperation', () => {
 
 		expect( doc.version ).to.equal( 1 );
 		expect( root.getChildCount() ).to.equal( 1 );
-		expect( root.getChild( 0 )._getAttrCount() ).to.equal( 2 );
+		expect( getIteratorCount( root.getChild( 0 ).getAttrs() ) ).to.equal( 2 );
 		expect( root.getChild( 0 ).hasAttr( fooAttr ) ).to.be.true;
 		expect( root.getChild( 0 ).hasAttr( barAttr ) ).to.be.true;
 	} );
@@ -192,9 +196,9 @@ describe( 'ChangeOperation', () => {
 
 		expect( doc.version ).to.equal( 2 );
 		expect( root.getChildCount() ).to.equal( 3 );
-		expect( root.getChild( 0 )._getAttrCount() ).to.equal( 0 );
-		expect( root.getChild( 1 )._getAttrCount() ).to.equal( 0 );
-		expect( root.getChild( 2 )._getAttrCount() ).to.equal( 0 );
+		expect( getIteratorCount( root.getChild( 0 ).getAttrs() ) ).to.equal( 0 );
+		expect( getIteratorCount( root.getChild( 1 ).getAttrs() ) ).to.equal( 0 );
+		expect( getIteratorCount( root.getChild( 2 ).getAttrs() ) ).to.equal( 0 );
 	} );
 
 	it( 'should undo changing attribute by applying reverse operation', () => {
@@ -218,11 +222,11 @@ describe( 'ChangeOperation', () => {
 
 		expect( doc.version ).to.equal( 2 );
 		expect( root.getChildCount() ).to.equal( 3 );
-		expect( root.getChild( 0 )._getAttrCount() ).to.equal( 1 );
+		expect( getIteratorCount( root.getChild( 0 ).getAttrs() ) ).to.equal( 1 );
 		expect( root.getChild( 0 ).hasAttr( oldAttr ) ).to.be.true;
-		expect( root.getChild( 1 )._getAttrCount() ).to.equal( 1 );
+		expect( getIteratorCount( root.getChild( 1 ).getAttrs() ) ).to.equal( 1 );
 		expect( root.getChild( 1 ).hasAttr( oldAttr ) ).to.be.true;
-		expect( root.getChild( 2 )._getAttrCount() ).to.equal( 1 );
+		expect( getIteratorCount( root.getChild( 2 ).getAttrs() ) ).to.equal( 1 );
 		expect( root.getChild( 2 ).hasAttr( oldAttr ) ).to.be.true;
 	} );
 
@@ -246,11 +250,11 @@ describe( 'ChangeOperation', () => {
 
 		expect( doc.version ).to.equal( 2 );
 		expect( root.getChildCount() ).to.equal( 3 );
-		expect( root.getChild( 0 )._getAttrCount() ).to.equal( 1 );
+		expect( getIteratorCount( root.getChild( 0 ).getAttrs() ) ).to.equal( 1 );
 		expect( root.getChild( 0 ).hasAttr( fooAttr ) ).to.be.true;
-		expect( root.getChild( 1 )._getAttrCount() ).to.equal( 1 );
+		expect( getIteratorCount( root.getChild( 1 ).getAttrs() ) ).to.equal( 1 );
 		expect( root.getChild( 1 ).hasAttr( fooAttr ) ).to.be.true;
-		expect( root.getChild( 2 )._getAttrCount() ).to.equal( 1 );
+		expect( getIteratorCount( root.getChild( 2 ).getAttrs() ) ).to.equal( 1 );
 		expect( root.getChild( 2 ).hasAttr( fooAttr ) ).to.be.true;
 	} );
 

+ 51 - 2
packages/ckeditor5-engine/tests/document/range.js

@@ -9,15 +9,23 @@
 
 const modules = bender.amd.require(
 	'document/range',
-	'document/position'
+	'document/position',
+	'document/element',
+	'document/character',
+	'document/document'
 );
 
 describe( 'Range', () => {
-	let Range, Position, start, end;
+	let Range, Position, Element, Character, Document;
+
+	let start, end;
 
 	before( () => {
 		Position = modules[ 'document/position' ];
 		Range = modules[ 'document/range' ];
+		Element = modules[ 'document/element' ];
+		Character = modules[ 'document/character' ];
+		Document = modules[ 'document/document' ];
 
 		start = new Position( [ 0 ] );
 		end = new Position( [ 1 ] );
@@ -66,4 +74,45 @@ describe( 'Range', () => {
 			expect( range.isEqual( diffRange ) ).to.not.be.true;
 		} );
 	} );
+
+	describe( 'static constructors', () => {
+		let doc, root, p, f, o, z;
+
+		// root
+		//  |- p
+		//     |- f
+		//     |- o
+		//     |- z
+		before( () => {
+			doc = new Document();
+
+			root = doc.createRoot( 'root' );
+
+			f = new Character( 'f' );
+			o = new Character( 'o' );
+			z = new Character( 'z' );
+
+			p = new Element( 'p', [], [ f, o, z ] );
+
+			root.insertChildren( 0, [ p ] );
+		} );
+
+		describe( 'createFromElement', () => {
+			it( 'should return range', () => {
+				const range = Range.createFromElement( p );
+
+				expect( range.start.path ).to.deep.equal( [ 0, 0 ] );
+				expect( range.end.path ).to.deep.equal( [ 0, 3 ] );
+			} );
+		} );
+
+		describe( 'createFromParentsAndOffsets', () => {
+			it( 'should return range', () => {
+				const range = Range.createFromParentsAndOffsets( root, 0, p, 2 );
+
+				expect( range.start.path ).to.deep.equal( [ 0 ] );
+				expect( range.end.path ).to.deep.equal( [ 0, 2 ] );
+			} );
+		} );
+	} );
 } );

+ 5 - 1
packages/ckeditor5-engine/tests/document/rootelement.js

@@ -7,8 +7,12 @@
 
 /* bender-tags: document */
 
+/* bender-include: ../_tools/tools.js */
+
 'use strict';
 
+const getIteratorCount = bender.tools.core.getIteratorCount;
+
 const modules = bender.amd.require(
 	'document/document',
 	'document/element',
@@ -31,7 +35,7 @@ describe( 'Element', () => {
 
 			expect( root ).to.be.an.instanceof( Element );
 			expect( root ).to.have.property( 'document' ).that.equals( doc );
-			expect( root._getAttrCount() ).to.equal( 0 );
+			expect( getIteratorCount( root.getAttrs() ) ).to.equal( 0 );
 			expect( root.getChildCount() ).to.equal( 0 );
 		} );
 	} );

+ 82 - 0
packages/ckeditor5-engine/tests/document/transaction.js

@@ -0,0 +1,82 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: document, delta */
+
+'use strict';
+
+const modules = bender.amd.require(
+	'document/transaction',
+	'document/delta/delta',
+	'ckeditorerror' );
+
+describe( 'Transaction', () => {
+	let Transaction, Delta, CKEditorError;
+
+	before( () => {
+		Transaction = modules[ 'document/transaction' ];
+		Delta = modules[ 'document/delta/delta' ];
+		CKEditorError = modules.ckeditorerror;
+	} );
+
+	it( 'should have registered basic methods', () => {
+		const transaction = new Transaction();
+
+		expect( transaction.setAttr ).to.be.a( 'function' );
+		expect( transaction.removeAttr ).to.be.a( 'function' );
+	} );
+
+	describe( 'Transaction.register', () => {
+		let TestDelta;
+
+		before( () => {
+			TestDelta = class extends Delta {
+				constructor( transaction ) {
+					super( transaction, [] );
+				}
+			};
+		} );
+
+		afterEach( () => {
+			delete Transaction.prototype.foo;
+		} );
+
+		it( 'should register function which return an delta', () => {
+			Transaction.register( 'foo', function() {
+				this.addDelta( new TestDelta() );
+			} );
+
+			const transaction = new Transaction();
+
+			transaction.foo();
+
+			expect( transaction.deltas.length ).to.equal( 1 );
+			expect( transaction.deltas[ 0 ] ).to.be.instanceof( TestDelta );
+		} );
+
+		it( 'should register function which return an multiple deltas', () => {
+			Transaction.register( 'foo', function() {
+				this.addDelta( new TestDelta() );
+				this.addDelta( new TestDelta() );
+			} );
+
+			const transaction = new Transaction();
+
+			transaction.foo();
+
+			expect( transaction.deltas.length ).to.equal( 2 );
+			expect( transaction.deltas[ 0 ] ).to.be.instanceof( TestDelta );
+			expect( transaction.deltas[ 1 ] ).to.be.instanceof( TestDelta );
+		} );
+
+		it( 'should throw if one try to register the same transaction twice', () => {
+			Transaction.register( 'foo', () => {} );
+
+			expect( () => {
+				Transaction.register( 'foo', () => {} );
+			} ).to.throw( CKEditorError, /^transaction-register-taken/ );
+		} );
+	} );
+} );