8
0
فهرست منبع

Added: core.treeModel.History.

Szymon Cofalik 9 سال پیش
والد
کامیت
f030b47ab1

+ 11 - 0
packages/ckeditor5-engine/src/treemodel/document.js

@@ -11,6 +11,7 @@ import transformations from './delta/basic-transformations.js'; // jshint ignore
 
 import RootElement from './rootelement.js';
 import Batch from './batch.js';
+import History from './history.js';
 import Selection from './selection.js';
 import EmitterMixin from '../../utils/emittermixin.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
@@ -93,6 +94,14 @@ export default class Document {
 
 		// Graveyard tree root. Document always have a graveyard root, which stores removed nodes.
 		this.createRoot( graveyardSymbol );
+
+		/**
+		 * Document's history.
+		 *
+		 * @readonly
+		 * @member {core.treeModel.History} core.treeModel.Document#history
+		 */
+		this.history = new History();
 	}
 
 	/**
@@ -131,6 +140,8 @@ export default class Document {
 
 		this.version++;
 
+		this.history.addOperation( operation );
+
 		const batch = operation.delta && operation.delta.batch;
 		this.fire( 'change', operation.type, changes, batch );
 	}

+ 126 - 0
packages/ckeditor5-engine/src/treemodel/history.js

@@ -0,0 +1,126 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+// Load all basic deltas and transformations, they register themselves, but they need to be imported somewhere.
+import deltas from './delta/basic-deltas.js';
+import transformations from './delta/basic-transformations.js';
+/*jshint unused: false*/
+
+import transform from './delta/transform.js';
+import CKEditorError from '../../utils/ckeditorerror.js';
+
+/**
+ * History keeps the track of all the deltas applied to the {@link core.treeModel.Document document} and provides
+ * utility tools to operate on the history. Most of times history is needed to transform a delta that has wrong
+ * {@link core.treeModel.delta.Delta#baseVersion} to a state where it can be applied to the document.
+ *
+ * @memberOf core.treeModel
+ */
+export default class History {
+	/**
+	 * Creates an empty History instance.
+	 */
+	constructor() {
+		/**
+		 * Deltas added to the history.
+		 *
+		 * @private
+		 * @member {Array.<core.treeModel.delta.Delta>} core.treeModel.History#_deltas
+		 */
+		this._deltas = [];
+
+		/**
+		 * Helper structure that maps added delta's base version to the index in {@link core.treeModel.History#_deltas}
+		 * at which the delta was added.
+		 *
+		 * @private
+		 * @member {Map} core.treeModel.History#_historyPoints
+		 */
+		this._historyPoints = new Map();
+	}
+
+	/**
+	 * Gets the number of base version which an up-to-date operation should have.
+	 *
+	 * @private
+	 * @type {Number}
+	 */
+	get _nextHistoryPoint() {
+		const lastDelta = this._deltas[ this._deltas.length - 1 ];
+
+		return lastDelta.baseVersion + lastDelta.operations.length;
+	}
+
+	/**
+	 * Adds an operation to the history.
+	 *
+	 * @param {core.treeModel.operation.Operation} operation Operation to add.
+	 */
+	addOperation( operation ) {
+		const delta = operation.delta;
+
+		// History cares about deltas not singular operations.
+		// Operations from a delta are added one by one, from first to last.
+		// Operations from one delta cannot be mixed with operations from other deltas.
+		// This all leads us to the conclusion that we could just save deltas history.
+		// What is more, we need to check only the last position in history to check if delta is already in the history.
+		if ( delta && this._deltas[ this._deltas.length - 1 ] !== delta ) {
+			const index = this._deltas.length;
+
+			this._deltas[ index ] = delta;
+			this._historyPoints.set( delta.baseVersion, index );
+		}
+	}
+
+	/**
+	 * Transforms out-dated delta by all deltas that were added to the history since the given delta's base version. In other
+	 * words, it makes the delta up-to-date with the history. The transformed delta(s) is (are) ready to be applied
+	 * to the {@link core.treeModel.Document document}.
+	 *
+	 * @param {core.treeModel.delta.Delta} delta Delta to update.
+	 * @returns {Array.<core.treeModel.delta.Delta>} Result of transformation which is an array containing one or more deltas.
+	 */
+	updateDelta( delta ) {
+		if ( delta.baseVersion === this._nextHistoryPoint ) {
+			return [ delta ];
+		}
+
+		let index = this._historyPoints.get( delta.baseVersion );
+
+		if ( index === undefined ) {
+			throw new CKEditorError( 'history-wrong-version: Cannot retrieve point in history that is a base for given delta.' );
+		}
+
+		let transformed = [ delta ];
+
+		while ( index < this._deltas.length ) {
+			const historyDelta = this._deltas[ index ];
+			let allResults = [];
+
+			for ( let deltaToTransform of transformed ) {
+				const transformedDelta = History._transform( deltaToTransform, historyDelta );
+				allResults = allResults.concat( transformedDelta );
+			}
+
+			transformed = allResults;
+			index++;
+		}
+
+		return transformed;
+	}
+
+	/**
+	 * Transforms given delta by another given delta.
+	 *
+	 * @private
+	 * @param {core.treeModel.delta.Delta} toTransform Delta to be transformed.
+	 * @param {core.treeModel.delta.Delta} transformBy Delta to transform by.
+	 */
+	static _transform( toTransform, transformBy ) {
+		return transform( toTransform, transformBy, false );
+	}
+}

+ 154 - 0
packages/ckeditor5-engine/tests/treemodel/history.js

@@ -0,0 +1,154 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import History from '/ckeditor5/core/treemodel/history.js';
+import Delta from '/ckeditor5/core/treemodel/delta/delta.js';
+import NoOperation from '/ckeditor5/core/treemodel/operation/nooperation.js';
+import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
+
+describe( 'History', () => {
+	let history;
+
+	beforeEach( () => {
+		history = new History();
+	} );
+
+	describe( 'constructor', () => {
+		it( 'should create an empty History instance', () => {
+			expect( history._deltas.length ).to.equal( 0 );
+			expect( history._historyPoints.size ).to.equal( 0 );
+		} );
+	} );
+
+	describe( 'addOperation', () => {
+		it( 'should save delta containing passed operation in the history', () => {
+			let delta = new Delta();
+			let operation = new NoOperation( 0 );
+
+			delta.addOperation( operation );
+			history.addOperation( operation );
+
+			expect( history._deltas.length ).to.equal( 1 );
+			expect( history._deltas[ 0 ] ).to.equal( delta );
+		} );
+
+		it( 'should save each delta only once', () => {
+			let delta = new Delta();
+
+			delta.addOperation( new NoOperation( 0 ) );
+			delta.addOperation( new NoOperation( 1 ) );
+			delta.addOperation( new NoOperation( 2 ) );
+
+			for ( let operation of delta.operations ) {
+				history.addOperation( operation );
+			}
+
+			expect( history._deltas.length ).to.equal( 1 );
+			expect( history._deltas[ 0 ] ).to.equal( delta );
+		} );
+
+		it( 'should save multiple deltas and keep their order', () => {
+			let deltaA = new Delta();
+			let deltaB = new Delta();
+			let deltaC = new Delta();
+
+			let deltas = [ deltaA, deltaB, deltaC ];
+
+			let i = 0;
+
+			for ( let delta of deltas ) {
+				delta.addOperation( new NoOperation( i++ ) );
+				delta.addOperation( new NoOperation( i++ ) );
+			}
+
+			for ( let delta of deltas ) {
+				for ( let operation of delta.operations ) {
+					history.addOperation( operation );
+				}
+			}
+
+			expect( history._deltas.length ).to.equal( 3 );
+			expect( history._deltas[ 0 ] ).to.equal( deltaA );
+			expect( history._deltas[ 1 ] ).to.equal( deltaB );
+			expect( history._deltas[ 2 ] ).to.equal( deltaC );
+		} );
+	} );
+
+	describe( 'updateDelta', () => {
+		it( 'should transform given delta by deltas from history which were applied since the baseVersion of given delta', () => {
+			sinon.spy( History, '_transform' );
+
+			let deltaA = new Delta();
+			deltaA.addOperation( new NoOperation( 0 ) );
+
+			let deltaB = new Delta();
+			deltaB.addOperation( new NoOperation( 1 ) );
+
+			let deltaC = new Delta();
+			deltaC.addOperation( new NoOperation( 2 ) );
+
+			let deltaD = new Delta();
+			deltaD.addOperation( new NoOperation( 3 ) );
+
+			let deltaX = new Delta();
+			deltaX.addOperation( new NoOperation( 1 ) );
+
+			history.addOperation( deltaA.operations[ 0 ] );
+			history.addOperation( deltaB.operations[ 0 ] );
+			history.addOperation( deltaC.operations[ 0 ] );
+			history.addOperation( deltaD.operations[ 0 ] );
+
+			// `deltaX` bases on the same history point as `deltaB` -- so it already acknowledges `deltaA` existence.
+			// It should be transformed by `deltaB` and all following deltas (`deltaC` and `deltaD`).
+			history.updateDelta( deltaX );
+
+			// `deltaX` was not transformed by `deltaA`.
+			expect( History._transform.calledWithExactly( deltaX, deltaA ) ).to.be.false;
+
+			expect( History._transform.calledWithExactly( deltaX, deltaB ) ).to.be.true;
+			// We can't do exact call matching because after first transformation, what we are further transforming
+			// is no longer `deltaX` but a result of transforming `deltaX` and `deltaB`.
+			expect( History._transform.calledWithExactly( sinon.match.instanceOf( Delta ), deltaC ) ).to.be.true;
+			expect( History._transform.calledWithExactly( sinon.match.instanceOf( Delta ), deltaD ) ).to.be.true;
+		} );
+
+		it( 'should not transform given delta if it bases on current version of history', () => {
+			let deltaA = new Delta();
+			deltaA.addOperation( new NoOperation( 0 ) );
+
+			let deltaB = new Delta();
+			let opB = new NoOperation( 1 );
+			deltaB.addOperation( opB );
+
+			history.addOperation( deltaA.operations[ 0 ] );
+
+			let result = history.updateDelta( deltaB );
+
+			expect( result.length ).to.equal( 1 );
+			expect( result[ 0 ] ).to.equal( deltaB );
+			expect( result[ 0 ].operations[ 0 ] ).to.equal( opB );
+		} );
+
+		it( 'should throw if given delta bases on an incorrect version of history', () => {
+			let deltaA = new Delta();
+			deltaA.addOperation( new NoOperation( 0 ) );
+			deltaA.addOperation( new NoOperation( 1 ) );
+
+			history.addOperation( deltaA.operations[ 0 ] );
+			history.addOperation( deltaA.operations[ 1 ] );
+
+			let deltaB = new Delta();
+			// Wrong base version - should be either 0 or 2, operation can't be based on an operation that is
+			// in the middle of other delta, because deltas are atomic, not dividable structures.
+			deltaB.addOperation( new NoOperation( 1 ) );
+
+			expect( () => {
+				history.updateDelta( deltaB );
+			} ).to.throw( CKEditorError, /history-wrong-version/ );
+		} );
+	} );
+} );