Parcourir la source

Merge branch 'master' into t/1667

Piotrek Koszuliński il y a 6 ans
Parent
commit
471116064c

+ 21 - 0
packages/ckeditor5-engine/src/conversion/upcastdispatcher.js

@@ -55,6 +55,27 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
  *			}
  *		} );
  *
+ *		// Convert <p>'s font-size style.
+ *		// Note: You should use a low-priority observer in order to ensure that
+ *		// it's executed after the element-to-element converter.
+ *		editor.data.upcastDispatcher.on( 'element:p', ( evt, data, conversionApi ) => {
+ *			const { consumable, schema, writer } = conversionApi;
+ *
+ *			if ( !consumable.consume( data.viewItem, { style: 'font-size' } ) ) {
+ *				return;
+ *			}
+ *
+ *			const fontSize = data.viewItem.getStyle( 'font-size' );
+ *
+ *			// Don't go for the model element after data.modelCursor because it might happen
+ *			// that a single view element was converted to multiple model elements. Get all of them.
+ *			for ( const item of data.modelRange.getItems( { shallow: true } ) ) {
+ *				if ( schema.checkAttribute( item, 'fontSize' ) ) {
+ *					writer.setAttribute( 'fontSize', fontSize, item );
+ *				}
+ *			}
+ *		}, { priority: 'low' } );
+ *
  *		// Convert all elements which have no custom converter into paragraph (autoparagraphing).
  *  	editor.data.upcastDispatcher.on( 'element', ( evt, data, conversionApi ) => {
  *  	 	// When element is already consumed by higher priority converters then do nothing.

+ 2 - 2
packages/ckeditor5-engine/src/conversion/upcasthelpers.js

@@ -156,7 +156,7 @@ export default class UpcastHelpers extends ConversionHelpers {
 	 * @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing
 	 * the model attribute. `value` property may be set as a function that takes a view element and returns the value.
 	 * If `String` is given, the model attribute value will be set to `true`.
-	 * @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
+	 * @param {module:utils/priorities~PriorityString} [config.converterPriority='low'] Converter priority.
 	 * @returns {module:engine/conversion/upcasthelpers~UpcastHelpers}
 	 */
 	elementToAttribute( config ) {
@@ -422,7 +422,7 @@ function upcastElementToElement( config ) {
 // @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing
 // the model attribute. `value` property may be set as a function that takes a view element and returns the value.
 // If `String` is given, the model attribute value will be set to `true`.
-// @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
+// @param {module:utils/priorities~PriorityString} [config.converterPriority='low'] Converter priority.
 // @returns {Function} Conversion helper.
 function upcastElementToAttribute( config ) {
 	config = cloneDeep( config );

+ 24 - 20
packages/ckeditor5-engine/src/model/operation/transform.js

@@ -148,6 +148,8 @@ 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.
@@ -155,9 +157,15 @@ export function transformSets( operationsA, operationsB, options ) {
 	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:
@@ -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;
 
@@ -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
@@ -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;
 
@@ -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.
@@ -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 );
 		}
 	}
 
@@ -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 );
@@ -637,7 +641,7 @@ 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.
@@ -645,7 +649,7 @@ class ContextFactory {
 			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.
@@ -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 );
 

+ 18 - 1
packages/ckeditor5-engine/src/view/upcastwriter.js

@@ -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!' );
@@ -173,6 +173,23 @@ export default class UpcastWriter {
 	}
 
 	/**
+	 * 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.
 	 *

+ 67 - 1
packages/ckeditor5-engine/tests/model/operation/transform.js

@@ -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';
@@ -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 - 0
packages/ckeditor5-engine/tests/view/upcastwriter.js

@@ -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 );