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

Commit

Permalink
Merge branch 'master' into t/ckeditor5/1151
Browse files Browse the repository at this point in the history
  • Loading branch information
oleq committed Aug 5, 2019
2 parents f60b745 + c057a94 commit 1821fd6
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 22 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
19 changes: 18 additions & 1 deletion src/view/upcastwriter.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import Selection from './selection';
* section of the {@glink framework/guides/architecture/editing-engine Editing engine architecture} guide.
*
* Unlike `DowncastWriter`, which is available in the {@link module:engine/view/view~View#change `View#change()`} block,
* `UpcastWriter` can wherever you need it:
* `UpcastWriter` can be created wherever you need it:
*
* const writer = new UpcastWriter();
* const text = writer.createText( 'foo!' );
Expand Down Expand Up @@ -172,6 +172,23 @@ export default class UpcastWriter {
return false;
}

/**
* Removes given element from view structure and places its children in its position.
* It does nothing if element has no parent.
*
* @param {module:engine/view/element~Element} element Element to unwrap.
*/
unwrapElement( element ) {
const parent = element.parent;

if ( parent ) {
const index = parent.getChildIndex( element );

this.remove( element );
this.insertChild( index, element.getChildren(), parent );
}
}

/**
* Renames element by creating a copy of a given element but with its name changed and then moving contents of the
* old element to the new one.
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 );
} );
} );
30 changes: 30 additions & 0 deletions tests/view/upcastwriter.js
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,36 @@ describe( 'UpcastWriter', () => {
} );
} );

describe( 'unwrapElement', () => {
it( 'should unwrap simple element', () => {
const documentFragment = dataprocessor.toView( '<ul><li><p>foo</p></li></ul>' );
const paragraph = documentFragment.getChild( 0 ).getChild( 0 ).getChild( 0 );

writer.unwrapElement( paragraph );

expect( dataprocessor.toData( documentFragment ) ).to.equal( '<ul><li>foo</li></ul>' );
} );

it( 'should unwrap element with children', () => {
const documentFragment = dataprocessor.toView(
'<p><span style="color:red"><strong>foo</strong><a href="example.com">example</a>bar</span></p>' );
const span = documentFragment.getChild( 0 ).getChild( 0 );

writer.unwrapElement( span );

expect( dataprocessor.toData( documentFragment ) ).to.equal(
'<p><strong>foo</strong><a href="example.com">example</a>bar</p>' );
} );

it( 'should do nothing for elements without parent', () => {
const element = new Element( 'p', null, 'foo' );

writer.unwrapElement( element );

expect( dataprocessor.toData( element ) ).to.equal( '<p>foo</p>' );
} );
} );

describe( 'rename', () => {
it( 'should rename simple element', () => {
const el = view.getChild( 0 ).getChild( 1 );
Expand Down

0 comments on commit 1821fd6

Please sign in to comment.