8
0
Quellcode durchsuchen

Merge pull request #727 from ckeditor/t/726

Added: Multiple deltas x deltas transformation utilities.
Piotrek Koszuliński vor 9 Jahren
Ursprung
Commit
f7d52ce39b

+ 78 - 0
packages/ckeditor5-engine/src/model/delta/transform.js

@@ -7,7 +7,9 @@
  * @module engine/model/delta/transform
  */
 
+import Delta from './delta.js';
 import operationTransform from '../operation/transform.js';
+import NoOperation from '../operation/nooperation.js';
 import arrayUtils from '../../../utils/lib/lodash/array.js';
 
 const specialCases = new Map();
@@ -217,3 +219,79 @@ function getPriority( A, B, isAMoreImportantThanB ) {
 		return isAMoreImportantThanB;
 	}
 }
+
+/**
+ * Transforms two sets of deltas by themselves. Returns both transformed sets. Does not modify passed parameters.
+ *
+ * @param {Array.<module:engine/model/delta/delta~Delta>} deltasA Array with first set of deltas to transform.
+ * @param {Array.<module:engine/model/delta/delta~Delta>} deltasB Array with second set of deltas to transform.
+ * @param {Boolean} isAMoreImportantThanB Flag indicating whether the deltas from `deltasA` set should be treated as more
+ * important when resolving conflicts.
+ * @returns {Object}
+ * @returns {Array.<module:engine/model/delta/delta~Delta>} return.deltasA The first set of deltas transformed by the second set of deltas.
+ * @returns {Array.<module:engine/model/delta/delta~Delta>} return.deltasB The second set of deltas transformed by the first set of deltas.
+ */
+export function transformDeltaSets( deltasA, deltasB, isAMoreImportantThanB ) {
+	let transformedDeltasA = Array.from( deltasA );
+	let transformedDeltasB = Array.from( deltasB );
+
+	for ( let i = 0; i < transformedDeltasA.length; i++ ) {
+		let deltaA = [ transformedDeltasA[ i ] ];
+
+		for ( let j = 0; j < transformedDeltasB.length; j++ ) {
+			let deltaB = [ transformedDeltasB[ j ] ];
+
+			for ( let k = 0; k < deltaA.length; k++ ) {
+				for ( let l = 0; l < deltaB.length; l++ ) {
+					let resultAB = transform( deltaA[ k ], deltaB[ l ], isAMoreImportantThanB );
+					let resultBA = transform( deltaB[ l ], deltaA[ k ], !isAMoreImportantThanB );
+
+					deltaA.splice( k, 1, ...resultAB );
+					k += resultAB.length - 1;
+
+					deltaB.splice( l, 1, ...resultBA );
+					l += resultBA.length - 1;
+				}
+			}
+
+			transformedDeltasB.splice( j, 1, ...deltaB );
+			j += deltaB.length - 1;
+		}
+
+		transformedDeltasA.splice( i, 1, ...deltaA );
+		i += deltaA.length - 1;
+	}
+
+	const opsDiffA = getOpsCount( transformedDeltasA ) - getOpsCount( deltasA );
+	const opsDiffB = getOpsCount( transformedDeltasB ) - getOpsCount( deltasB );
+
+	if ( opsDiffB < opsDiffA ) {
+		padWithNoOps( transformedDeltasB, opsDiffA - opsDiffB );
+	} else if ( opsDiffA < opsDiffB ) {
+		padWithNoOps( transformedDeltasA, opsDiffB - opsDiffA );
+	}
+
+	return { deltasA: transformedDeltasA, deltasB: transformedDeltasB };
+}
+
+// Returns number of operations in given array of deltas.
+function getOpsCount( deltas ) {
+	return deltas.reduce( ( current, delta ) => {
+		return current + delta.operations.length;
+	}, 0 );
+}
+
+// Adds a delta containing `howMany` `NoOperation` instances to given array with deltas.
+// Used to "synchronize" the number of operations in two delta sets.
+function padWithNoOps( deltas, howMany ) {
+	const lastDelta = deltas[ deltas.length - 1 ];
+	let baseVersion = lastDelta.operations.length + lastDelta.baseVersion;
+
+	let noDelta = new Delta();
+
+	for ( let i = 0; i < howMany; i++ ) {
+		noDelta.addOperation( new NoOperation( baseVersion++ ) );
+	}
+
+	deltas.push( noDelta );
+}

+ 351 - 0
packages/ckeditor5-engine/tests/model/delta/transform/transform.js

@@ -0,0 +1,351 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: model, operation */
+
+import transformations from 'ckeditor5/engine/model/delta/basic-transformations.js';
+/*jshint unused: false*/
+
+import { transformDeltaSets } from 'ckeditor5/engine/model/delta/transform.js';
+
+import Document from 'ckeditor5/engine/model/document.js';
+import Element from 'ckeditor5/engine/model/element.js';
+import Text from 'ckeditor5/engine/model/text.js';
+import Position from 'ckeditor5/engine/model/position.js';
+import Range from 'ckeditor5/engine/model/range.js';
+
+import Delta from 'ckeditor5/engine/model/delta/delta.js';
+import InsertDelta from 'ckeditor5/engine/model/delta/insertdelta.js';
+import RemoveDelta from 'ckeditor5/engine/model/delta/removedelta.js';
+import SplitDelta from 'ckeditor5/engine/model/delta/splitdelta.js';
+
+import NoOperation from 'ckeditor5/engine/model/operation/nooperation.js';
+import MoveOperation from 'ckeditor5/engine/model/operation/moveoperation.js';
+import RemoveOperation from 'ckeditor5/engine/model/operation/removeoperation.js';
+import InsertOperation from 'ckeditor5/engine/model/operation/insertoperation.js';
+
+import {
+	expectDelta,
+	getInsertDelta,
+	getSplitDelta,
+	getRemoveDelta
+} from 'tests/engine/model/delta/transform/_utils/utils.js';
+
+describe( 'transform', () => {
+	let doc, root, gy, baseVersion;
+
+	beforeEach( () => {
+		doc = new Document();
+		root = doc.createRoot();
+
+		root.appendChildren( new Element( 'p', null, new Text( 'foobar' ) ) );
+
+		gy = doc.graveyard;
+		baseVersion = doc.version;
+	} );
+
+	describe( 'transformDeltaSets', () => {
+		it( 'should transform two deltas', () => {
+			const insertDelta = getInsertDelta( new Position( root, [ 0, 4 ] ), new Text( 'xxx' ), baseVersion );
+			const removeDelta = getRemoveDelta( new Position( root, [ 0, 0 ] ), 2, baseVersion );
+
+			const { deltasA, deltasB } = transformDeltaSets( [ insertDelta ], [ removeDelta ] );
+
+			expect( deltasA.length ).to.equal( 1 );
+			expect( deltasB.length ).to.equal( 1 );
+
+			expectDelta( deltasA[ 0 ], {
+				type: InsertDelta,
+				operations: [
+					{
+						type: InsertOperation,
+						position: new Position( root, [ 0, 2 ] ),
+						baseVersion: 1
+					}
+				]
+			} );
+
+			expectDelta( deltasB[ 0 ], {
+				type: RemoveDelta,
+				operations: [
+					{
+						type: RemoveOperation,
+						sourcePosition: new Position( root, [ 0, 0 ] ),
+						howMany: 2,
+						baseVersion: 1
+					}
+				]
+			} );
+		} );
+
+		it( 'should transform two deltas - reverse', () => {
+			const insertDelta = getInsertDelta( new Position( root, [ 0, 4 ] ), new Text( 'xxx' ), baseVersion );
+			const removeDelta = getRemoveDelta( new Position( root, [ 0, 0 ] ), 2, baseVersion );
+
+			const { deltasA, deltasB } = transformDeltaSets( [ removeDelta ], [ insertDelta ] );
+
+			expect( deltasA.length ).to.equal( 1 );
+			expect( deltasB.length ).to.equal( 1 );
+
+			expectDelta( deltasA[ 0 ], {
+				type: RemoveDelta,
+				operations: [
+					{
+						type: RemoveOperation,
+						sourcePosition: new Position( root, [ 0, 0 ] ),
+						howMany: 2,
+						baseVersion: 1
+					}
+				]
+			} );
+
+			expectDelta( deltasB[ 0 ], {
+				type: InsertDelta,
+				operations: [
+					{
+						type: InsertOperation,
+						position: new Position( root, [ 0, 2 ] ),
+						baseVersion: 1
+					}
+				]
+			} );
+		} );
+
+		it( 'should transform two arrays of deltas', () => {
+			const splitDelta = getSplitDelta( new Position( root, [ 0, 3 ] ), new Element( 'p' ), 3, baseVersion );
+			const insertDeltaX = getInsertDelta( new Position( root, [ 0, 3 ] ), new Text( 'xxx' ), baseVersion + 2 );
+
+			const removeDelta = getRemoveDelta( new Position( root, [ 0, 2 ] ), 2, baseVersion );
+			const insertDeltaY = getInsertDelta( new Position( root, [ 0, 4 ] ), new Text( 'yyy' ), baseVersion + 1 );
+
+			const { deltasA, deltasB } = transformDeltaSets( [ splitDelta, insertDeltaX ], [ removeDelta, insertDeltaY ], true );
+
+			expect( deltasA.length ).to.equal( 3 );
+			expect( deltasB.length ).to.equal( 2 );
+
+			expectDelta( deltasA[ 0 ], {
+				type: SplitDelta,
+				operations: [
+					{
+						type: InsertOperation,
+						position: new Position( root, [ 1 ] ),
+						baseVersion: 2
+					},
+					{
+						type: MoveOperation,
+						sourcePosition: new Position( root, [ 0, 2 ] ),
+						howMany: 5,
+						targetPosition: new Position( root, [ 1, 0 ] ),
+						baseVersion: 3
+					}
+				]
+			} );
+
+			expectDelta( deltasA[ 1 ], {
+				type: InsertDelta,
+				operations: [
+					{
+						type: InsertOperation,
+						position: new Position( root, [ 0, 2 ] ),
+						baseVersion: 4
+					}
+				]
+			} );
+
+			expectDelta( deltasA[ 2 ], {
+				type: Delta,
+				operations: [
+					{
+						type: NoOperation,
+						baseVersion: 5
+					}
+				]
+			} );
+
+			expectDelta( deltasB[ 0 ], {
+				type: RemoveDelta,
+				operations: [
+					{
+						type: RemoveOperation,
+						sourcePosition: new Position( root, [ 1, 0 ] ),
+						howMany: 1,
+						baseVersion: 3
+					},
+					{
+						type: RemoveOperation,
+						sourcePosition: new Position( root, [ 0, 2 ] ),
+						howMany: 1,
+						baseVersion: 4
+					}
+				]
+			} );
+
+			expectDelta( deltasB[ 1 ], {
+				type: InsertDelta,
+				operations: [
+					{
+						type: InsertOperation,
+						position: new Position( root, [ 1, 2 ] ),
+						baseVersion: 5
+					}
+				]
+			} );
+		} );
+
+		it( 'should transform two arrays of deltas - reverse', () => {
+			const splitDelta = getSplitDelta( new Position( root, [ 0, 3 ] ), new Element( 'p' ), 3, baseVersion );
+			const insertDeltaX = getInsertDelta( new Position( root, [ 0, 3 ] ), new Text( 'xxx' ), baseVersion + 2 );
+
+			const removeDelta = getRemoveDelta( new Position( root, [ 0, 2 ] ), 2, baseVersion );
+			const insertDeltaY = getInsertDelta( new Position( root, [ 0, 4 ] ), new Text( 'yyy' ), baseVersion + 1 );
+
+			const { deltasA, deltasB } = transformDeltaSets( [ removeDelta, insertDeltaY ], [ splitDelta, insertDeltaX ], true );
+
+			expect( deltasA.length ).to.equal( 2 );
+			expect( deltasB.length ).to.equal( 3 );
+
+			expectDelta( deltasA[ 0 ], {
+				type: RemoveDelta,
+				operations: [
+					{
+						type: RemoveOperation,
+						sourcePosition: new Position( root, [ 1, 0 ] ),
+						howMany: 1,
+						baseVersion: 3
+					},
+					{
+						type: RemoveOperation,
+						sourcePosition: new Position( root, [ 0, 2 ] ),
+						howMany: 1,
+						baseVersion: 4
+					}
+				]
+			} );
+
+			expectDelta( deltasA[ 1 ], {
+				type: InsertDelta,
+				operations: [
+					{
+						type: InsertOperation,
+						position: new Position( root, [ 1, 2 ] ),
+						baseVersion: 5
+					}
+				]
+			} );
+
+			expectDelta( deltasB[ 0 ], {
+				type: SplitDelta,
+				operations: [
+					{
+						type: InsertOperation,
+						position: new Position( root, [ 1 ] ),
+						baseVersion: 2
+					},
+					{
+						type: MoveOperation,
+						sourcePosition: new Position( root, [ 0, 2 ] ),
+						howMany: 5,
+						targetPosition: new Position( root, [ 1, 0 ] ),
+						baseVersion: 3
+					}
+				]
+			} );
+
+			expectDelta( deltasB[ 1 ], {
+				type: InsertDelta,
+				operations: [
+					{
+						type: InsertOperation,
+						position: new Position( root, [ 0, 2 ] ),
+						baseVersion: 4
+					}
+				]
+			} );
+
+			expectDelta( deltasB[ 2 ], {
+				type: Delta,
+				operations: [
+					{
+						type: NoOperation,
+						baseVersion: 5
+					}
+				]
+			} );
+		} );
+
+		it( 'importance flag - first set is important', () => {
+			const insertDeltaA = getInsertDelta( new Position( root, [ 0, 4 ] ), new Text( 'xxx' ), baseVersion );
+			const insertDeltaB = getInsertDelta( new Position( root, [ 0, 4 ] ), new Text( 'yyy' ), baseVersion );
+
+			let { deltasA, deltasB } = transformDeltaSets( [ insertDeltaA ], [ insertDeltaB ], true );
+
+			expectDelta( deltasA[ 0 ], {
+				type: InsertDelta,
+				operations: [
+					{
+						type: InsertOperation,
+						position: new Position( root, [ 0, 4 ] ),
+						baseVersion: 1
+					}
+				]
+			} );
+
+			expectDelta( deltasB[ 0 ], {
+				type: InsertDelta,
+				operations: [
+					{
+						type: InsertOperation,
+						position: new Position( root, [ 0, 7 ] ),
+						baseVersion: 1
+					}
+				]
+			} );
+		} );
+
+		it( 'importance flag - second set is important', () => {
+			const insertDeltaA = getInsertDelta( new Position( root, [ 0, 4 ] ), new Text( 'xxx' ), baseVersion );
+			const insertDeltaB = getInsertDelta( new Position( root, [ 0, 4 ] ), new Text( 'yyy' ), baseVersion );
+
+			let { deltasA, deltasB } = transformDeltaSets( [ insertDeltaA ], [ insertDeltaB ], false );
+
+			expectDelta( deltasA[ 0 ], {
+				type: InsertDelta,
+				operations: [
+					{
+						type: InsertOperation,
+						position: new Position( root, [ 0, 7 ] ),
+						baseVersion: 1
+					}
+				]
+			} );
+
+			expectDelta( deltasB[ 0 ], {
+				type: InsertDelta,
+				operations: [
+					{
+						type: InsertOperation,
+						position: new Position( root, [ 0, 4 ] ),
+						baseVersion: 1
+					}
+				]
+			} );
+		} );
+
+		it( 'should not modify original deltas or arrays', () => {
+			const insertDeltaA = getInsertDelta( new Position( root, [ 0, 0 ] ), new Text( 'x' ), baseVersion );
+			const insertDeltaB = getInsertDelta( new Position( root, [ 1, 0 ] ), new Text( 'y' ), baseVersion );
+
+			const originalDeltasA = [ insertDeltaA ];
+			const originalDeltasB = [ insertDeltaB ];
+
+			let { deltasA, deltasB } = transformDeltaSets( originalDeltasA, originalDeltasB, false );
+
+			expect( deltasA ).to.not.equal( originalDeltasA );
+			expect( deltasB ).to.not.equal( originalDeltasB );
+			expect( deltasA[ 0 ] ).to.not.equal( originalDeltasA[ 0 ] );
+			expect( deltasB[ 0 ] ).to.not.equal( originalDeltasB[ 0 ] );
+		} );
+	} );
+} );