Kaynağa Gözat

Introduce operations.

Piotr Jasiun 10 yıl önce
ebeveyn
işleme
3a7346fd76

+ 102 - 0
packages/ckeditor5-utils/src/document/changeoperation.js

@@ -0,0 +1,102 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [ 'document/operation' ], function( Operation ) {
+	/**
+	 *
+	 *
+	 * @class document.Operation
+	 */
+	class ChangeOperation extends Operation {
+		/**
+		 *
+		 */
+		constructor( range, oldAttr, newAttr, baseVersion ) {
+			super( baseVersion );
+
+			this.range = range;
+			this.oldAttr = oldAttr;
+			this.newAttr = newAttr;
+		}
+
+		_execute() {
+			var range = this.range;
+			var oldAttr = this.oldAttr;
+			var newAttr = this.newAttr;
+
+			if ( newAttr === null ) {
+				remove();
+			} else if ( oldAttr === null ) {
+				insert();
+			} else {
+				change();
+			}
+
+			function remove() {
+				var attrs, i, len;
+
+				for ( var value of range ) {
+					// TODO: if debug
+					if ( !value.node.hasAttr( oldAttr ) ) {
+						throw 'The attribute which should be removed does not exists.';
+					}
+
+					attrs = value.node.attrs;
+
+					for ( i = 0, len = attrs.length; i < len; i++ ) {
+						if ( attrs[ i ].isEquals( oldAttr ) ) {
+							attrs.splice( i, 1 );
+						}
+					}
+				}
+			}
+
+			function insert() {
+				for ( var value of range ) {
+					// TODO: if debug
+					if ( value.node.hasAttr( newAttr.key ) ) {
+						throw 'The attribute with given key already exists.';
+					}
+
+					value.node.attrs.push( newAttr );
+				}
+			}
+
+			function change() {
+				var attrs, i, len;
+
+				for ( var value of range ) {
+					// TODO: if debug
+					if ( oldAttr.key != newAttr.key ) {
+						throw 'Old and new attributes should have the same keys.';
+					}
+
+					// TODO: if debug
+					if ( !value.node.hasAttr( oldAttr ) ) {
+						throw 'The attribute which should be changed does not exists.';
+					}
+
+					attrs = value.node.attrs;
+
+					for ( i = 0, len = attrs.length; i < len; i++ ) {
+						if ( attrs[ i ].isEquals( oldAttr ) ) {
+							attrs.splice( i, 1 );
+						}
+					}
+
+					attrs.push( newAttr );
+				}
+			}
+		}
+
+		reverseOperation() {
+			return new ChangeOperation( this.range, this.newAttr, this.oldAttr, this.baseVersion + 1 );
+		}
+	}
+
+	return ChangeOperation;
+} );

+ 14 - 3
packages/ckeditor5-utils/src/document/document.js

@@ -5,7 +5,7 @@
 
 'use strict';
 
-CKEDITOR.define( [ 'document/element' ], function( Element ) {
+CKEDITOR.define( [ 'document/element', 'emittermixin', 'utils' ], function( Element, EmitterMixin, utils ) {
 	/**
 	 * Document model.
 	 *
@@ -23,16 +23,27 @@ CKEDITOR.define( [ 'document/element' ], function( Element ) {
 			 * @property {String} root
 			 */
 			this.root = new Element( null, 'root' );
+
+			this.version = 0;
 		}
 
 		/**
 		 * This is the only entry point for all document changes.
 		 *
-		 * @param {document.Element} operation Operation to be applied.
+		 * @param {document.Operation} operation Operation to be applied.
 		 */
-		applyOperation() {
+		applyOperation( operation ) {
+			if ( operation.baseVersion !== this.version ) {
+				throw 'Only operations with matching versions can be applied.';
+			}
+
+			operation._execute();
+			this.version++;
+			this.fire( 'operationApplied', operation );
 		}
 	}
 
+	utils.extend( Document.prototype, EmitterMixin );
+
 	return Document;
 } );

+ 39 - 0
packages/ckeditor5-utils/src/document/insertoperation.js

@@ -0,0 +1,39 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [ 'document/operation', 'document/removeoperation' ], function( Operation ) {
+	/**
+	 *
+	 *
+	 * @class document.Operation
+	 */
+	class InsertOperation extends Operation {
+		/**
+		 *
+		 */
+		constructor( position, nodes, baseVersion ) {
+			super( baseVersion );
+
+			this.position = position;
+			this.nodes = nodes;
+		}
+
+		_execute() {
+			var children = this.position.parent.children;
+
+			children.splice.apply( children, [ this.position.offset, 0 ].concat( Operation.uncompress( this.nodes ) ) );
+		}
+
+		reverseOperation() {
+			var RemoveOperation = CKEDITOR.require( 'document/removeoperation' );
+
+			return new RemoveOperation( this.position, this.nodes, this.baseVersion + 1 );
+		}
+	}
+
+	return InsertOperation;
+} );

+ 41 - 0
packages/ckeditor5-utils/src/document/moveoperation.js

@@ -0,0 +1,41 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [ 'document/operation' ], function( Operation ) {
+	/**
+	 *
+	 *
+	 * @class document.Operation
+	 */
+	class MoveOperation extends Operation {
+		/**
+		 *
+		 */
+		constructor( sourcePosition, targetPosition, nodes, baseVersion ) {
+			super( baseVersion );
+
+			this.sourcePosition = sourcePosition;
+			this.targetPosition = targetPosition;
+			this.nodes = nodes;
+		}
+
+		_execute() {
+			var children = this.position.parent.children;
+			var nodes = this.uncompres( this.nodes );
+
+			children.splice( this.position.offset, nodes.length );
+
+			children.splice.apply( children, [ this.position.offset, 0 ].concat( nodes ) );
+		}
+
+		reverseOperation() {
+			return new MoveOperation( this.targetPosition, this.nodes, this.sourcePosition, this.baseVersion + 1 );
+		}
+	}
+
+	return MoveOperation;
+} );

+ 25 - 1
packages/ckeditor5-utils/src/document/node.js

@@ -5,7 +5,7 @@
 
 'use strict';
 
-CKEDITOR.define( function() {
+CKEDITOR.define( [ 'document/attribute' ], function( Attribute ) {
 	/**
 	 * Abstract document tree node class.
 	 *
@@ -37,6 +37,30 @@ CKEDITOR.define( function() {
 			this.attrs = attrs || [];
 		}
 
+		hasAttr( attr ) {
+			return this.getAttr( attr ) !== null;
+		}
+
+		getAttr( attr ) {
+			var i, len;
+
+			if ( attr instanceof Attribute ) {
+				for ( i = 0, len = this.attrs.length; i < len; i++ ) {
+					if ( this.attrs[ i ].isEquals( attr ) ) {
+						return this.attrs[ i ];
+					}
+				}
+			} else {
+				for ( i = 0, len = this.attrs.length; i < len; i++ ) {
+					if ( this.attrs[ i ].key == attr ) {
+						return this.attrs[ i ];
+					}
+				}
+			}
+
+			return null;
+		}
+
 		/**
 		 * Position of the node in the parent element.
 		 *

+ 47 - 0
packages/ckeditor5-utils/src/document/operation.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/character', 'document/node', 'utils' ], function( Character, Node, utils ) {
+	/**
+	 *
+	 *
+	 * @class document.Operation
+	 */
+	class Operation {
+		/**
+		 *
+		 */
+		constructor( baseVersion ) {
+			this.baseVersion = baseVersion;
+		}
+
+		static uncompress( nodes ) {
+			var uncompress = [];
+			var node;
+
+			if ( !utils.isArray( nodes ) ) {
+				nodes = [ nodes ];
+			}
+
+			for ( var i = 0, nodesLen = nodes.length; i < nodesLen; i++ ) {
+				node = nodes[ i ];
+
+				if ( node instanceof Node ) {
+					uncompress.push( node );
+				} else {
+					for ( var j = 0, nodeLen = node.length; j < nodeLen; j++ ) {
+						uncompress.push( new Character( null, node[ j ] ) );
+					}
+				}
+			}
+
+			return uncompress;
+		}
+	}
+
+	return Operation;
+} );

+ 37 - 0
packages/ckeditor5-utils/src/document/removeoperation.js

@@ -0,0 +1,37 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+CKEDITOR.define( [ 'document/operation', 'document/insertoperation' ], function( Operation ) {
+	/**
+	 *
+	 *
+	 * @class document.Operation
+	 */
+	class RemoveOperation extends Operation {
+		/**
+		 *
+		 */
+		constructor( position, nodes, baseVersion ) {
+			super( baseVersion );
+
+			this.position = position;
+			this.nodes = nodes;
+		}
+
+		_execute() {
+			this.position.parent.children.splice( this.position.offset, Operation.uncompress( this.nodes ).length );
+		}
+
+		reverseOperation() {
+			var InsertOperation = CKEDITOR.require( 'document/insertoperation' );
+
+			return new InsertOperation( this.position, this.nodes, this.baseVersion + 1 );
+		}
+	}
+
+	return RemoveOperation;
+} );

+ 35 - 0
packages/ckeditor5-utils/tests/document/changeoperation.js

@@ -0,0 +1,35 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: document */
+
+'use strict';
+
+describe( 'ChangeOperation', function() {
+	it( 'should insert attribute' );
+
+	it( 'should change attribute' );
+
+	it( 'should remove attribute' );
+
+	it( 'should create a change operation as a reverse' );
+
+	it( 'should undo changes by applying reverse operation' );
+
+	it( 'should undo insert attribute by applying reverse operation' );
+
+	it( 'should undo change attribute by applying reverse operation' );
+
+	it( 'should undo remove attribute by applying reverse operation' );
+
+	it( 'should throw an error when one try to remove and the attribute does not exists' );
+
+	it( 'should throw an error when one try to insert and the attribute already exists' );
+
+	it( 'should throw an error when one try to change and the new and old attributes have different keys' );
+
+	it( 'should throw an error when one try to change and the old attribute does not exists' );
+} );
+

+ 37 - 2
packages/ckeditor5-utils/tests/document/document.js

@@ -3,8 +3,6 @@
  * For licensing, see LICENSE.md.
  */
 
-/* globals describe, it, expect, bender */
-
 /* bender-tags: document */
 
 'use strict';
@@ -21,4 +19,41 @@ describe( 'constructor', function() {
 		expect( document ).to.have.property( 'root' ).that.is.instanceof( Element );
 		expect( document.root ).to.have.property( 'name' ).that.equal( 'root' );
 	} );
+} );
+
+describe( 'applyOperation', function() {
+	it( 'should increase document version, execute operation and fire operationApplied', function() {
+		var Document = modules[ 'document/document' ];
+
+		var document = new Document();
+		var operationApplied = sinon.spy();
+		var operation = {
+				baseVersion: 0,
+				_execute: sinon.spy()
+			};
+
+		document.on( 'operationApplied', operationApplied );
+
+		document.applyOperation( operation );
+
+		expect( document.version ).to.be.equal( 1 );
+		sinon.assert.calledOnce( operationApplied );
+		sinon.assert.calledOnce( operation._execute );
+	} );
+
+	it( 'should throw an error on the operation base version and the document version is different', function() {
+		var Document = modules[ 'document/document' ];
+
+		var document = new Document();
+		var operationApplied = sinon.spy();
+		var operation = {
+				baseVersion: 1
+			};
+
+		document.on( 'operationApplied', operationApplied );
+
+		expect( function() {
+			document.applyOperation( operation );
+		} ).to.throw();
+	} );
 } );

+ 151 - 0
packages/ckeditor5-utils/tests/document/insertoperation.js

@@ -0,0 +1,151 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: document */
+
+'use strict';
+
+var modules = bender.amd.require(
+	'document/document',
+	'document/insertoperation',
+	'document/removeoperation',
+	'document/position',
+	'document/character' );
+
+describe( 'InsertOperation', function() {
+	it( 'should insert node', function() {
+		var Document = modules[ 'document/document' ];
+		var InsertOperation = modules[ 'document/insertoperation' ];
+		var Position = modules[ 'document/position' ];
+		var Character = modules[ 'document/character' ];
+
+		var doc = new Document();
+
+		doc.applyOperation( new InsertOperation(
+			new Position( [ 0 ], doc ),
+			new Character( null, 'x' ),
+			doc.version ) );
+
+		expect( doc.version ).to.be.equal( 1 );
+		expect( doc.root.children.length ).to.be.equal( 1 );
+		expect( doc.root.children[ 0 ].character ).to.be.equal( 'x' );
+	} );
+
+	it( 'should insert set of nodes', function() {
+		var Document = modules[ 'document/document' ];
+		var InsertOperation = modules[ 'document/insertoperation' ];
+		var Position = modules[ 'document/position' ];
+		var Character = modules[ 'document/character' ];
+
+		var doc = new Document();
+
+		doc.applyOperation( new InsertOperation(
+			new Position( [ 0 ], doc ),
+			[ new Character( null, 'b' ), new Character( null, 'a' ), new Character( null, 'r' ) ],
+			doc.version ) );
+
+		expect( doc.version ).to.be.equal( 1 );
+		expect( doc.root.children.length ).to.be.equal( 3 );
+		expect( doc.root.children[ 0 ].character ).to.be.equal( 'b' );
+		expect( doc.root.children[ 1 ].character ).to.be.equal( 'a' );
+		expect( doc.root.children[ 2 ].character ).to.be.equal( 'r' );
+	} );
+
+	it( 'should insert text', function() {
+		var Document = modules[ 'document/document' ];
+		var InsertOperation = modules[ 'document/insertoperation' ];
+		var Position = modules[ 'document/position' ];
+		var Character = modules[ 'document/character' ];
+
+		var doc = new Document();
+
+		doc.applyOperation( new InsertOperation(
+			new Position( [ 0 ], doc ),
+			[ 'foo', new Character( null, 'x' ), 'bar' ],
+			doc.version ) );
+
+		expect( doc.version ).to.be.equal( 1 );
+		expect( doc.root.children.length ).to.be.equal( 7 );
+		expect( doc.root.children[ 0 ].character ).to.be.equal( 'f' );
+		expect( doc.root.children[ 1 ].character ).to.be.equal( 'o' );
+		expect( doc.root.children[ 2 ].character ).to.be.equal( 'o' );
+		expect( doc.root.children[ 3 ].character ).to.be.equal( 'x' );
+		expect( doc.root.children[ 4 ].character ).to.be.equal( 'b' );
+		expect( doc.root.children[ 5 ].character ).to.be.equal( 'a' );
+		expect( doc.root.children[ 6 ].character ).to.be.equal( 'r' );
+	} );
+
+	it( 'should create a remove operation as a reverse', function() {
+		var Document = modules[ 'document/document' ];
+		var InsertOperation = modules[ 'document/insertoperation' ];
+		var RemoveOperation = modules[ 'document/removeoperation' ];
+		var Position = modules[ 'document/position' ];
+		var Character = modules[ 'document/character' ];
+
+		var doc = new Document();
+
+		var nodes = [ new Character( null, 'b' ), new Character( null, 'a' ), new Character( null, 'r' ) ];
+		var position = new Position( [ 0 ], doc );
+
+		var operation = new InsertOperation( position, nodes, 0 );
+
+		var reverse = operation.reverseOperation();
+
+		expect( reverse ).to.be.an.instanceof( RemoveOperation );
+		expect( reverse.baseVersion ).to.be.equals( 1 );
+		expect( reverse.nodes ).to.be.equals( nodes );
+		expect( reverse.position ).to.be.equals( position );
+	} );
+
+	it( 'should undo insert node by applying reverse operation', function() {
+		var Document = modules[ 'document/document' ];
+		var InsertOperation = modules[ 'document/insertoperation' ];
+		var Position = modules[ 'document/position' ];
+		var Character = modules[ 'document/character' ];
+
+		var doc = new Document();
+
+		var operation = new InsertOperation(
+			new Position( [ 0 ], doc ),
+			new Character( null, 'x' ),
+			doc.version );
+
+		var reverse = operation.reverseOperation();
+
+		doc.applyOperation( operation );
+
+		expect( doc.version ).to.be.equal( 1 );
+
+		doc.applyOperation( reverse );
+
+		expect( doc.version ).to.be.equal( 2 );
+		expect( doc.root.children.length ).to.be.equal( 0 );
+	} );
+
+	it( 'should undo insert set of nodes by applying reverse operation', function() {
+		var Document = modules[ 'document/document' ];
+		var InsertOperation = modules[ 'document/insertoperation' ];
+		var Position = modules[ 'document/position' ];
+		var Character = modules[ 'document/character' ];
+
+		var doc = new Document();
+
+		var operation = new InsertOperation(
+			new Position( [ 0 ], doc ),
+			[ new Character( null, 'b' ), new Character( null, 'a' ), new Character( null, 'r' ) ],
+			doc.version );
+
+		var reverse = operation.reverseOperation();
+
+		doc.applyOperation( operation );
+
+		expect( doc.version ).to.be.equal( 1 );
+
+		doc.applyOperation( reverse );
+
+		expect( doc.version ).to.be.equal( 2 );
+		expect( doc.root.children.length ).to.be.equal( 0 );
+	} );
+} );

+ 20 - 0
packages/ckeditor5-utils/tests/document/moveoperation.js

@@ -0,0 +1,20 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: document */
+
+'use strict';
+
+describe( 'MoveOperation', function() {
+	it( 'should move node' );
+
+	it( 'should move set of nodes' );
+
+	it( 'should create a move operation as a reverse' );
+
+	it( 'should move node by applying reverse operation' );
+
+	it( 'should move set of nodes by applying reverse operation' );
+} );

+ 49 - 0
packages/ckeditor5-utils/tests/document/operation.js

@@ -0,0 +1,49 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: document */
+
+'use strict';
+
+var modules = bender.amd.require( 'document/operation', 'document/character' );
+
+describe( 'uncompress', function() {
+	it( 'should change array of strings into a set of nodes', function() {
+		var Operation = modules[ 'document/operation' ];
+		var Character = modules[ 'document/character' ];
+
+		var uncompressed = Operation.uncompress( [ 'foo', new Character( null, 'x' ), 'bar' ] );
+
+		expect( uncompressed.length ).to.be.equal( 7 );
+		expect( uncompressed[ 0 ].character ).to.be.equal( 'f' );
+		expect( uncompressed[ 1 ].character ).to.be.equal( 'o' );
+		expect( uncompressed[ 2 ].character ).to.be.equal( 'o' );
+		expect( uncompressed[ 3 ].character ).to.be.equal( 'x' );
+		expect( uncompressed[ 4 ].character ).to.be.equal( 'b' );
+		expect( uncompressed[ 5 ].character ).to.be.equal( 'a' );
+		expect( uncompressed[ 6 ].character ).to.be.equal( 'r' );
+	} );
+
+	it( 'should change string into a set of nodes', function() {
+		var Operation = modules[ 'document/operation' ];
+
+		var uncompressed = Operation.uncompress( 'foo' );
+
+		expect( uncompressed.length ).to.be.equal( 3 );
+		expect( uncompressed[ 0 ].character ).to.be.equal( 'f' );
+		expect( uncompressed[ 1 ].character ).to.be.equal( 'o' );
+		expect( uncompressed[ 2 ].character ).to.be.equal( 'o' );
+	} );
+
+	it( 'should change node into a set of nodes', function() {
+		var Operation = modules[ 'document/operation' ];
+		var Character = modules[ 'document/character' ];
+
+		var uncompressed = Operation.uncompress( new Character( null, 'x' ) );
+
+		expect( uncompressed.length ).to.be.equal( 1 );
+		expect( uncompressed[ 0 ].character ).to.be.equal( 'x' );
+	} );
+} );

+ 20 - 0
packages/ckeditor5-utils/tests/document/removeoperation.js

@@ -0,0 +1,20 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: document */
+
+'use strict';
+
+describe( 'RemoveOperation', function() {
+	it( 'should remove node' );
+
+	it( 'should remove set of nodes' );
+
+	it( 'should create a insert operation as a reverse' );
+
+	it( 'should undo remove node by applying reverse operation' );
+
+	it( 'should undo remove set of nodes by applying reverse operation' );
+} );