Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
Merge pull request #1768 from ckeditor/t/1767
Browse files Browse the repository at this point in the history
Feature: `transformSets()` will now return a `Map` instance linking transformed operations to the original operations.
  • Loading branch information
scofalik authored Jul 31, 2019
2 parents ad9159c + 50d87d3 commit 61da3ec
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 21 deletions.
44 changes: 24 additions & 20 deletions src/model/operation/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,16 +148,24 @@ export function transform( a, b, context = {} ) {
* @returns {Object} Transformation result.
* @returns {Array.<module:engine/model/operation/operation~Operation>} return.operationsA Transformed `operationsA`.
* @returns {Array.<module:engine/model/operation/operation~Operation>} return.operationsB Transformed `operationsB`.
* @returns {Map} return.originalOperations A map that links transformed operations to original operations. The keys are the transformed
* operations and the values are the original operations from the input (`operationsA` and `operationsB`).
*/
export function transformSets( operationsA, operationsB, options ) {
// Create new arrays so the originally passed arguments are not changed.
// No need to clone operations, they are cloned as they are transformed.
operationsA = operationsA.slice();
operationsB = operationsB.slice();

const contextFactory = new ContextFactory( options.document, options.useRelations, options.forceWeakRemove );
contextFactory.setOriginalOperations( operationsA );
contextFactory.setOriginalOperations( operationsB );

const originalOperations = contextFactory.originalOperations;

// If one of sets is empty there is simply nothing to transform, so return sets as they are.
if ( operationsA.length == 0 || operationsB.length == 0 ) {
return { operationsA, operationsB };
return { operationsA, operationsB, originalOperations };
}
//
// Following is a description of transformation process:
Expand Down Expand Up @@ -305,10 +313,6 @@ export function transformSets( operationsA, operationsB, options ) {
originalOperationsBCount: operationsB.length
};

const contextFactory = new ContextFactory( options.document, options.useRelations, options.forceWeakRemove );
contextFactory.setOriginalOperations( operationsA );
contextFactory.setOriginalOperations( operationsB );

// Index of currently transformed operation `a`.
let i = 0;

Expand Down Expand Up @@ -374,7 +378,7 @@ export function transformSets( operationsA, operationsB, options ) {
updateBaseVersions( operationsA, data.nextBaseVersionB );
updateBaseVersions( operationsB, data.nextBaseVersionA );

return { operationsA, operationsB };
return { operationsA, operationsB, originalOperations };
}

// Gathers additional data about operations processed during transformation. Can be used to obtain contextual information
Expand All @@ -388,6 +392,13 @@ class ContextFactory {
// @param {Boolean} [forceWeakRemove=false] If set to `false`, remove operation will be always stronger than move operation,
// so the removed nodes won't end up back in the document root. When set to `true`, context data will be used.
constructor( document, useRelations, forceWeakRemove = false ) {
// For each operation that is created during transformation process, we keep a reference to the original operation
// which it comes from. The original operation works as a kind of "identifier". Every contextual information
// gathered during transformation that we want to save for given operation, is actually saved for the original operation.
// This way no matter if operation `a` is cloned, then transformed, even breaks, we still have access to the previously
// gathered data through original operation reference.
this.originalOperations = new Map();

// `model.History` instance which information about undone operations will be taken from.
this._history = document.history;

Expand All @@ -396,13 +407,6 @@ class ContextFactory {

this._forceWeakRemove = !!forceWeakRemove;

// For each operation that is created during transformation process, we keep a reference to the original operation
// which it comes from. The original operation works as a kind of "identifier". Every contextual information
// gathered during transformation that we want to save for given operation, is actually saved for the original operation.
// This way no matter if operation `a` is cloned, then transformed, even breaks, we still have access to the previously
// gathered data through original operation reference.
this._originalOperations = new Map();

// Relations is a double-map structure (maps in map) where for two operations we store how those operations were related
// to each other. Those relations are evaluated during transformation process. For every transformated pair of operations
// we keep relations between them.
Expand All @@ -428,10 +432,10 @@ class ContextFactory {
// @param {Array.<module:engine/model/operation/operation~Operation>} operations
// @param {module:engine/model/operation/operation~Operation|null} [takeFrom=null]
setOriginalOperations( operations, takeFrom = null ) {
const originalOperation = takeFrom ? this._originalOperations.get( takeFrom ) : null;
const originalOperation = takeFrom ? this.originalOperations.get( takeFrom ) : null;

for ( const operation of operations ) {
this._originalOperations.set( operation, originalOperation || operation );
this.originalOperations.set( operation, originalOperation || operation );
}
}

Expand Down Expand Up @@ -605,7 +609,7 @@ class ContextFactory {
// For `op`, get its original operation. After all, if `op` is a clone (or even transformed clone) of another
// operation, literally `op` couldn't be undone. It was just generated. If anything, it was the operation it origins
// from which was undone. So get that original operation.
const originalOp = this._originalOperations.get( op );
const originalOp = this.originalOperations.get( op );

// And check with the document if the original operation was undone.
return originalOp.wasUndone || this._history.isUndoneOperation( originalOp );
Expand Down Expand Up @@ -637,15 +641,15 @@ class ContextFactory {
// @returns {String|null}
_getRelation( opA, opB ) {
// Get the original operation. Similarly as in `wasUndone()` it is used as an universal identifier for stored data.
const origB = this._originalOperations.get( opB );
const origB = this.originalOperations.get( opB );
const undoneB = this._history.getUndoneOperation( origB );

// If `opB` is not undoing any operation, there is no relation.
if ( !undoneB ) {
return null;
}

const origA = this._originalOperations.get( opA );
const origA = this.originalOperations.get( opA );
const relationsA = this._relations.get( origA );

// Get all relations for `opA`, and check if there is a relation with `opB`-undone-counterpart. If so, return it.
Expand All @@ -664,8 +668,8 @@ class ContextFactory {
// @param {String} relation
_setRelation( opA, opB, relation ) {
// As always, setting is for original operations, not the clones/transformed operations.
const origA = this._originalOperations.get( opA );
const origB = this._originalOperations.get( opB );
const origA = this.originalOperations.get( opA );
const origB = this.originalOperations.get( opB );

let relationsA = this._relations.get( origA );

Expand Down
68 changes: 67 additions & 1 deletion tests/model/operation/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

import { transform } from '../../../src/model/operation/transform';
import { transform, transformSets } from '../../../src/model/operation/transform';

import Model from '../../../src/model/model';
import RootElement from '../../../src/model/rootelement';
Expand Down Expand Up @@ -2607,3 +2607,69 @@ describe( 'transform', () => {
} );
} );
} );

describe( 'transformSets', () => {
let model, doc, root, node;

beforeEach( () => {
model = new Model();
doc = model.document;
root = doc.createRoot();

node = new Node();
} );

it( 'originalOperations should correctly link transformed operations with original operations #1', () => {
const position = new Position( root, [ 0 ] );

const a = new InsertOperation( position, [ node ], 0 );

const { operationsA, originalOperations } = transformSets( [ a ], [], {
document: doc,
useRelations: false,
padWithNoOps: false
} );

expect( originalOperations.get( operationsA[ 0 ] ) ).to.equal( a );
} );

it( 'originalOperations should correctly link transformed operations with original operations #2', () => {
const position = new Position( root, [ 0 ] );

const b = new InsertOperation( position, [ node ], 0 );

const { operationsB, originalOperations } = transformSets( [], [ b ], {
document: doc,
useRelations: false,
padWithNoOps: false
} );

expect( originalOperations.get( operationsB[ 0 ] ) ).to.equal( b );
} );

it( 'originalOperations should correctly link transformed operations with original operations #3', () => {
const position = new Position( root, [ 4 ] );

const a = new InsertOperation( position, [ node ], 0 );
const b = new AttributeOperation(
new Range(
new Position( root, [ 2 ] ),
new Position( root, [ 11 ] )
),
'foo',
'bar',
'xyz',
0
);

const { operationsA, operationsB, originalOperations } = transformSets( [ a ], [ b ], {
document: doc,
useRelations: false,
padWithNoOps: false
} );

expect( originalOperations.get( operationsA[ 0 ] ) ).to.equal( a );
expect( originalOperations.get( operationsB[ 0 ] ) ).to.equal( b );
expect( originalOperations.get( operationsB[ 1 ] ) ).to.equal( b );
} );
} );

0 comments on commit 61da3ec

Please sign in to comment.