Przeglądaj źródła

Introduced additional conversion options for downcast conversion.

Oskar Wróbel 5 lat temu
rodzic
commit
0b2044e9af

+ 1 - 0
packages/ckeditor5-core/src/editor/utils/dataapimixin.js

@@ -76,5 +76,6 @@ export default DataApiMixin;
  * @param {String} [options.trim='empty'] Whether returned data should be trimmed. This option is set to `'empty'` by default,
  * which means that whenever editor content is considered empty, an empty string is returned. To turn off trimming
  * use `'none'`. In such cases exact content will be returned (for example `'<p>&nbsp;</p>'` for an empty editor).
+ * @param {Object} [options.conversionOptions] Additional, custom configuration passed to the conversion process.
  * @returns {String} Output data.
  */

+ 10 - 7
packages/ckeditor5-engine/src/controller/datacontroller.js

@@ -151,10 +151,11 @@ export default class DataController {
 	 * @param {String} [options.trim='empty'] Whether returned data should be trimmed. This option is set to `empty` by default,
 	 * which means whenever editor content is considered empty, an empty string will be returned. To turn off trimming completely
 	 * use `'none'`. In such cases exact content will be returned (for example `<p>&nbsp;</p>` for an empty editor).
+	 * @param {Object} [options.conversionOptions] Additional, custom configuration passed to the conversion process.
 	 * @returns {String} Output data.
 	 */
 	get( options ) {
-		const { rootName = 'main', trim = 'empty' } = options || {};
+		const { rootName = 'main', trim = 'empty', conversionOptions } = options || {};
 
 		if ( !this._checkIfRootsExists( [ rootName ] ) ) {
 			/**
@@ -177,7 +178,7 @@ export default class DataController {
 			return '';
 		}
 
-		return this.stringify( root );
+		return this.stringify( root, conversionOptions );
 	}
 
 	/**
@@ -187,11 +188,12 @@ export default class DataController {
 	 *
 	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} modelElementOrFragment
 	 * Element whose content will be stringified.
+	 * @param {Object} [conversionOptions] Additional, custom configuration passed to the conversion process.
 	 * @returns {String} Output data.
 	 */
-	stringify( modelElementOrFragment ) {
+	stringify( modelElementOrFragment, conversionOptions ) {
 		// Model -> view.
-		const viewDocumentFragment = this.toView( modelElementOrFragment );
+		const viewDocumentFragment = this.toView( modelElementOrFragment, conversionOptions );
 
 		// View -> data.
 		return this.processor.toData( viewDocumentFragment );
@@ -205,9 +207,10 @@ export default class DataController {
 	 *
 	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} modelElementOrFragment
 	 * Element or document fragment whose content will be converted.
+	 * @param {Object} [conversionOptions] Additional, custom configuration passed to the conversion process.
 	 * @returns {module:engine/view/documentfragment~DocumentFragment} Output view DocumentFragment.
 	 */
-	toView( modelElementOrFragment ) {
+	toView( modelElementOrFragment, conversionOptions ) {
 		const viewDocument = this.viewDocument;
 		const viewWriter = this._viewWriter;
 
@@ -221,7 +224,7 @@ export default class DataController {
 		this.mapper.bindElements( modelElementOrFragment, viewDocumentFragment );
 
 		// We have no view controller and rendering to DOM in DataController so view.change() block is not used here.
-		this.downcastDispatcher.convertInsert( modelRange, viewWriter );
+		this.downcastDispatcher.convertInsert( modelRange, viewWriter, conversionOptions );
 
 		if ( !modelElementOrFragment.is( 'documentFragment' ) ) {
 			// Then, if a document element is converted, convert markers.
@@ -229,7 +232,7 @@ export default class DataController {
 			const markers = _getMarkersRelativeToElement( modelElementOrFragment );
 
 			for ( const [ name, range ] of markers ) {
-				this.downcastDispatcher.convertMarkerAdd( name, range, viewWriter );
+				this.downcastDispatcher.convertMarkerAdd( name, range, viewWriter, conversionOptions );
 			}
 		}
 

+ 16 - 4
packages/ckeditor5-engine/src/conversion/downcastdispatcher.js

@@ -11,7 +11,6 @@ import Consumable from './modelconsumable';
 import Range from '../model/range';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
-import { extend } from 'lodash-es';
 
 /**
  * Downcast dispatcher is a central point of downcasting (conversion from the model to the view), which is a process of reacting to changes
@@ -115,7 +114,7 @@ export default class DowncastDispatcher {
 		 *
 		 * @member {module:engine/conversion/downcastdispatcher~DowncastConversionApi}
 		 */
-		this.conversionApi = extend( { dispatcher: this }, conversionApi );
+		this.conversionApi = Object.assign( { dispatcher: this }, conversionApi );
 	}
 
 	/**
@@ -166,9 +165,12 @@ export default class DowncastDispatcher {
 	 * @fires attribute
 	 * @param {module:engine/model/range~Range} range The inserted range.
 	 * @param {module:engine/view/downcastwriter~DowncastWriter} writer The view writer that should be used to modify the view document.
+	 * @param {Object} [options={}] Additional, custom configuration passed to the converters through
+	 * {module:engine/conversion/downcastdispatcher~DowncastConversionApi conversion api}. Used only with `dataDowncast` group.
 	 */
-	convertInsert( range, writer ) {
+	convertInsert( range, writer, options = {} ) {
 		this.conversionApi.writer = writer;
+		this.conversionApi.options = options;
 
 		// Create a list of things that can be consumed, consisting of nodes and their attributes.
 		this.conversionApi.consumable = this._createInsertConsumable( range );
@@ -319,14 +321,17 @@ export default class DowncastDispatcher {
 	 * @param {String} markerName Marker name.
 	 * @param {module:engine/model/range~Range} markerRange Marker range.
 	 * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
+	 * @param {Object} [options={}] Additional, custom configuration passed to the converters through
+	 * {module:engine/conversion/downcastdispatcher~DowncastConversionApi conversion api}. Used only with `dataDowncast` group.
 	 */
-	convertMarkerAdd( markerName, markerRange, writer ) {
+	convertMarkerAdd( markerName, markerRange, writer, options = {} ) {
 		// Do not convert if range is in graveyard or not in the document (e.g. in DocumentFragment).
 		if ( !markerRange.root.document || markerRange.root.rootName == '$graveyard' ) {
 			return;
 		}
 
 		this.conversionApi.writer = writer;
+		this.conversionApi.options = options;
 
 		// In markers' case, event name == consumable name.
 		const eventName = 'addMarker:' + markerName;
@@ -482,6 +487,7 @@ export default class DowncastDispatcher {
 	_clearConversionApi() {
 		delete this.conversionApi.writer;
 		delete this.conversionApi.consumable;
+		delete this.conversionApi.options;
 	}
 
 	/**
@@ -669,3 +675,9 @@ function shouldMarkerChangeBeConverted( modelPosition, marker, mapper ) {
  *
  * @member {module:engine/view/downcastwriter~DowncastWriter} #writer
  */
+
+/**
+ * An object with an additional, custom configuration used during conversion process. Available only for `dataDowncast` group.
+ *
+ * @member {Object} #options
+ */

+ 108 - 0
packages/ckeditor5-engine/tests/controller/datacontroller.js

@@ -454,6 +454,82 @@ describe( 'DataController', () => {
 				data.get( { rootName: 'nonexistent' } );
 			}, /datacontroller-get-non-existent-root:/ );
 		} );
+
+		it( 'should allow to provide additional conversion options - insert conversion', () => {
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+
+			data.downcastDispatcher.on( 'insert:paragraph', ( evt, data, conversionApi ) => {
+				conversionApi.consumable.consume( data.item, 'insert' );
+
+				const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
+				const viewElement = conversionApi.writer.createContainerElement( 'p', {
+					attribute: conversionApi.options.attributeValue
+				} );
+
+				conversionApi.mapper.bindElements( data.item, viewElement );
+				conversionApi.writer.insert( viewPosition, viewElement );
+			}, { priority: 'high' } );
+
+			setData( model, '<paragraph>foo</paragraph>' );
+
+			expect( data.get( { conversionOptions: { attributeValue: 'foo' } } ) ).to.equal( '<p attribute="foo">foo</p>' );
+			expect( data.get( { conversionOptions: { attributeValue: 'bar' } } ) ).to.equal( '<p attribute="bar">foo</p>' );
+		} );
+
+		it( 'should allow to provide additional conversion options - attribute conversion', () => {
+			schema.register( 'paragraph', { inheritAllFrom: '$block', allowAttributes: [ 'foo' ] } );
+			downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
+
+			data.downcastDispatcher.on( 'attribute:foo', ( evt, data, conversionApi ) => {
+				if ( data.attributeNewValue === conversionApi.options.skipAttribute ) {
+					return;
+				}
+
+				const viewRange = conversionApi.mapper.toViewRange( data.range );
+				const viewElement = conversionApi.writer.createAttributeElement( data.attributeNewValue );
+
+				conversionApi.writer.wrap( viewRange, viewElement );
+			} );
+
+			setData( model, '<paragraph>f<$text foo="a">o</$text>ob<$text foo="b">a</$text>r</paragraph>' );
+
+			expect( data.get() ).to.equal( '<p>f<a>o</a>ob<b>a</b>r</p>' );
+			expect( data.get( { conversionOptions: { skipAttribute: 'a' } } ) ).to.equal( '<p>foob<b>a</b>r</p>' );
+			expect( data.get( { conversionOptions: { skipAttribute: 'b' } } ) ).to.equal( '<p>f<a>o</a>obar</p>' );
+		} );
+
+		it( 'should allow to provide additional conversion options - addMarker conversion', () => {
+			schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+			downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } );
+
+			data.downcastDispatcher.on( 'addMarker', ( evt, data, conversionApi ) => {
+				if ( conversionApi.options.skipMarker ) {
+					return;
+				}
+
+				const viewElement = conversionApi.writer.createAttributeElement( 'marker' );
+				const viewRange = conversionApi.mapper.toViewRange( data.markerRange );
+
+				conversionApi.writer.wrap( viewRange, viewElement );
+			} );
+
+			setData( model, '<paragraph>foo</paragraph>' );
+
+			const root = model.document.getRoot();
+
+			model.change( writer => {
+				const start = writer.createPositionFromPath( root, [ 0, 1 ] );
+				const end = writer.createPositionFromPath( root, [ 0, 2 ] );
+
+				writer.addMarker( 'marker', {
+					range: writer.createRange( start, end ),
+					usingOperation: false
+				} );
+			} );
+
+			expect( data.get( { conversionOptions: { skipMarker: false } } ) ).to.equal( '<p>f<marker>o</marker>o</p>' );
+			expect( data.get( { conversionOptions: { skipMarker: true } } ) ).to.equal( '<p>foo</p>' );
+		} );
 	} );
 
 	describe( 'stringify()', () => {
@@ -478,6 +554,22 @@ describe( 'DataController', () => {
 
 			expect( data.stringify( modelDocumentFragment ) ).to.equal( '<p>foo</p><p>bar</p>' );
 		} );
+
+		it( 'should allow to provide additional options to the conversion process', () => {
+			const spy = sinon.spy();
+
+			data.downcastDispatcher.on( 'insert:paragraph', ( evt, data, conversionApi ) => {
+				spy( conversionApi.options );
+			}, { priority: 'high' } );
+
+			const modelDocumentFragment = parseModel( '<paragraph>foo</paragraph><paragraph>bar</paragraph>', schema );
+
+			data.stringify( modelDocumentFragment );
+			expect( spy.lastCall.args[ 0 ] ).to.deep.equal( {} );
+
+			data.stringify( modelDocumentFragment, { foo: 'bar' } );
+			expect( spy.lastCall.args[ 0 ] ).to.deep.equal( { foo: 'bar' } );
+		} );
 	} );
 
 	describe( 'toView()', () => {
@@ -590,6 +682,22 @@ describe( 'DataController', () => {
 			expect( mappedViewRange.end.nodeBefore ).to.equal( firstViewElement );
 			expect( mappedViewRange.end.nodeAfter ).to.equal( viewDocumentFragment.getChild( 1 ) );
 		} );
+
+		it( 'should allow to provide additional options to the conversion process', () => {
+			const spy = sinon.spy();
+
+			data.downcastDispatcher.on( 'insert:paragraph', ( evt, data, conversionApi ) => {
+				spy( conversionApi.options );
+			}, { priority: 'high' } );
+
+			const modelDocumentFragment = parseModel( '<paragraph>foo</paragraph><paragraph>bar</paragraph>', schema );
+
+			data.toView( modelDocumentFragment );
+			expect( spy.lastCall.args[ 0 ] ).to.deep.equal( {} );
+
+			data.toView( modelDocumentFragment, { foo: 'bar' } );
+			expect( spy.lastCall.args[ 0 ] ).to.deep.equal( { foo: 'bar' } );
+		} );
 	} );
 
 	describe( 'destroy()', () => {

+ 41 - 0
packages/ckeditor5-engine/tests/conversion/downcastdispatcher.js

@@ -257,6 +257,31 @@ describe( 'DowncastDispatcher', () => {
 			expect( dispatcher.fire.calledWith( 'attribute:bold:image' ) ).to.be.false;
 			expect( dispatcher.fire.calledWith( 'insert:caption' ) ).to.be.false;
 		} );
+
+		it( 'should allow to provide an additional options', () => {
+			root._appendChild( [
+				new ModelText( 'foo', { bold: true } )
+			] );
+
+			const range = model.createRangeIn( root );
+			const conversionOptions = { foo: 'bar' };
+			const spy = sinon.spy();
+
+			dispatcher.on( 'insert', ( evt, data, conversionApi ) => {
+				expect( conversionApi.options ).to.equal( conversionOptions );
+				spy();
+			} );
+
+			dispatcher.on( 'attribute', ( evt, data, conversionApi ) => {
+				expect( conversionApi.options ).to.equal( conversionOptions );
+				spy();
+			} );
+
+			dispatcher.convertInsert( range, {}, conversionOptions );
+
+			// To be sure all assertions was checked.
+			sinon.assert.calledTwice( spy );
+		} );
 	} );
 
 	describe( 'convertRemove', () => {
@@ -630,6 +655,22 @@ describe( 'DowncastDispatcher', () => {
 			// Called once for each item, twice total.
 			expect( highAddMarkerSpy.calledTwice ).to.be.true;
 		} );
+
+		it( 'should allow to provide an additional options', () => {
+			const range = model.createRange( model.createPositionAt( element, 2 ), model.createPositionAt( element, 2 ) );
+			const conversionOptions = { foo: 'bar' };
+			const spy = sinon.spy();
+
+			dispatcher.on( 'addMarker:name', ( evt, data, conversionApi ) => {
+				expect( conversionApi.options ).to.equal( conversionOptions );
+				spy();
+			} );
+
+			dispatcher.convertMarkerAdd( 'name', range, {}, conversionOptions );
+
+			// To be sure all assertions was checked.
+			sinon.assert.called( spy );
+		} );
 	} );
 
 	describe( 'convertMarkerRemove', () => {