diff --git a/src/conversion/upcastdispatcher.js b/src/conversion/upcastdispatcher.js
index ca7eba966..0379df5db 100644
--- a/src/conversion/upcastdispatcher.js
+++ b/src/conversion/upcastdispatcher.js
@@ -108,15 +108,14 @@ export default class UpcastDispatcher {
*/
constructor( conversionApi = {} ) {
/**
- * List of elements that will be checked after conversion process and if element in the list will be empty it
- * will be removed from conversion result.
+ * List of the elements that were created during splitting.
*
- * After conversion process list is cleared.
+ * After conversion process the list is cleared.
*
- * @protected
- * @type {Set}
+ * @private
+ * @type {Map.>}
*/
- this._removeIfEmpty = new Set();
+ this._splitParts = new Map();
/**
* Position in the temporary structure where the converted content is inserted. The structure reflect the context of
@@ -140,6 +139,7 @@ export default class UpcastDispatcher {
this.conversionApi.convertItem = this._convertItem.bind( this );
this.conversionApi.convertChildren = this._convertChildren.bind( this );
this.conversionApi.splitToAllowedParent = this._splitToAllowedParent.bind( this );
+ this.conversionApi.getSplitParts = this._getSplitParts.bind( this );
}
/**
@@ -176,15 +176,15 @@ export default class UpcastDispatcher {
// Do the conversion.
const { modelRange } = this._convertItem( viewItem, this._modelCursor );
- // Conversion result is always a document fragment so let's create this fragment.
+ // Conversion result is always a document fragment so let's create it.
const documentFragment = writer.createDocumentFragment();
// When there is a conversion result.
if ( modelRange ) {
- // Remove all empty elements that was added to #_removeIfEmpty list.
+ // Remove all empty elements that were create while splitting.
this._removeEmptyElements();
- // Move all items that was converted to context tree to document fragment.
+ // Move all items that were converted in context tree to the document fragment.
for ( const item of Array.from( this._modelCursor.parent.getChildren() ) ) {
writer.append( item, documentFragment );
}
@@ -196,8 +196,8 @@ export default class UpcastDispatcher {
// Clear context position.
this._modelCursor = null;
- // Clear split elements.
- this._removeIfEmpty.clear();
+ // Clear split elements lists.
+ this._splitParts.clear();
// Clear conversion API.
this.conversionApi.writer = null;
@@ -283,14 +283,31 @@ export default class UpcastDispatcher {
// Split element to allowed parent.
const splitResult = this.conversionApi.writer.split( modelCursor, allowedParent );
- // Remember all elements that are created as a result of split.
- // This is important because at the end of conversion we want to remove all empty split elements.
+ // Using the range returned by `model.Writer#split`, we will pair original elements with their split parts.
+ //
+ // The range returned from the writer spans "over the split" or, precisely saying, from the end of the original element (the one
+ // that got split) to the beginning of the other part of that element:
+ //
+ // X[]Y ->
+ // X[]Y
//
- // Loop through positions between elements in range (except split result position) and collect parents.
- // [pos][pos][omit][pos][pos]
- for ( const position of splitResult.range.getPositions() ) {
- if ( !position.isEqual( splitResult.position ) ) {
- this._removeIfEmpty.add( position.parent );
+ // After the split there cannot be any full node between the positions in `splitRange`. The positions are touching.
+ // Also, because of how splitting works, it is easy to notice, that "closing tags" are in the reverse order than "opening tags".
+ // Also, since we split all those elements, each of them has to have the other part.
+ //
+ // With those observations in mind, we will pair the original elements with their split parts by saving "closing tags" and matching
+ // them with "opening tags" in the reverse order. For that we can use a stack.
+ const stack = [];
+
+ for ( const treeWalkerValue of splitResult.range.getWalker() ) {
+ if ( treeWalkerValue.type == 'elementEnd' ) {
+ stack.push( treeWalkerValue.item );
+ } else {
+ // There should not be any text nodes after the element is split, so the only other value is `elementStart`.
+ const originalPart = stack.pop();
+ const splitPart = treeWalkerValue.item;
+
+ this._registerSplitPair( originalPart, splitPart );
}
}
@@ -301,25 +318,62 @@ export default class UpcastDispatcher {
}
/**
- * Checks if {@link #_removeIfEmpty} contains empty elements and remove them.
- * We need to do it smart because there could be elements that are not empty because contains
- * other empty elements and after removing its children they become available to remove.
- * We need to continue iterating over split elements as long as any element will be removed.
+ * Registers that `splitPart` element is a split part of the `originalPart` element.
+ *
+ * Data set by this method is used by {@link #_getSplitParts} and {@link #_removeEmptyElements}.
+ *
+ * @private
+ * @param {module:engine/model/element~Element} originalPart
+ * @param {module:engine/model/element~Element} splitPart
+ */
+ _registerSplitPair( originalPart, splitPart ) {
+ if ( !this._splitParts.has( originalPart ) ) {
+ this._splitParts.set( originalPart, [ originalPart ] );
+ }
+
+ const list = this._splitParts.get( originalPart );
+
+ this._splitParts.set( splitPart, list );
+ list.push( splitPart );
+ }
+
+ /**
+ * @private
+ * @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#getSplitParts
+ */
+ _getSplitParts( element ) {
+ let parts;
+
+ if ( !this._splitParts.has( element ) ) {
+ parts = [ element ];
+ } else {
+ parts = this._splitParts.get( element );
+ }
+
+ return parts;
+ }
+
+ /**
+ * Checks if there are any empty elements created while splitting and removes them.
+ *
+ * This method works recursively to re-check empty elements again after at least one element was removed in the initial call,
+ * as some elements might have become empty after other empty elements were removed from them.
*
* @private
*/
_removeEmptyElements() {
- let removed = false;
+ let anyRemoved = false;
- for ( const element of this._removeIfEmpty ) {
+ for ( const element of this._splitParts.keys() ) {
if ( element.isEmpty ) {
this.conversionApi.writer.remove( element );
- this._removeIfEmpty.delete( element );
- removed = true;
+ this._splitParts.delete( element );
+
+ anyRemoved = true;
}
}
- if ( removed ) {
+ if ( anyRemoved ) {
this._removeEmptyElements();
}
}
@@ -408,7 +462,7 @@ function extractMarkersFromModelFragment( modelItem, writer ) {
return markers;
}
-// Creates model fragment according to given context and returns position in top element.
+// Creates model fragment according to given context and returns position in the bottom (the deepest) element.
function createContextTree( contextDefinition, writer ) {
let position;
@@ -465,7 +519,7 @@ function createContextTree( contextDefinition, writer ) {
* @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
* @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:text
* @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:documentFragment
- * @param {module:engine/view/item~Item} viewItem Item to convert.
+ * @param {module:engine/view/item~Item} viewItem Element which children should be converted.
* @param {module:engine/model/position~Position} modelCursor Position of conversion.
* @returns {Object} result Conversion result.
* @returns {module:engine/model/range~Range} result.modelRange Model range containing results of conversion of all children of given item.
@@ -504,6 +558,46 @@ function createContextTree( contextDefinition, writer ) {
* continue conversion. When element is not defined it means that there was no split.
*/
+/**
+ * Returns all the split parts of given `element` that were created during upcasting through using {@link #splitToAllowedParent}.
+ * It enables you to easily track those elements and continue processing them after they are split during their children conversion.
+ *
+ * Foobarbaz ->
+ * Foobarbaz
+ *
+ * For a reference to any of above paragraphs, the function will return all three paragraphs (the original element included),
+ * sorted in the order of their creation (the original element is the first one).
+ *
+ * If given `element` was not split, an array with single element is returned.
+ *
+ * Example of a usage in a converter code:
+ *
+ * const myElement = conversionApi.writer.createElement( 'myElement' );
+ *
+ * // Children conversion may split `myElement`.
+ * conversionApi.convertChildren( myElement, modelCursor );
+ *
+ * const splitParts = conversionApi.getSplitParts( myElement );
+ * const lastSplitPart = splitParts[ splitParts.length - 1 ];
+ *
+ * // Setting `data.modelRange` basing on split parts:
+ * data.modelRange = conversionApi.writer.createRange(
+ * conversionApi.writer.createPositionBefore( myElement ),
+ * conversionApi.writer.createPositionAfter( lastSplitPart )
+ * );
+ *
+ * // Setting `data.modelCursor` to continue after the last split element:
+ * data.modelCursor = conversionApi.writer.createPositionAfter( lastSplitPart );
+ *
+ * **Tip:** if you are unable to get a reference to the original element (for example because the code is split into multiple converters
+ * or even classes) but it was already converted, you might want to check first element in `data.modelRange`. This is a common situation
+ * if an attribute converter is separated from an element converter.
+ *
+ * @method #getSplitParts
+ * @param {module:engine/model/element~Element} element
+ * @returns {Array.}
+ */
+
/**
* Stores information about what parts of processed view item are still waiting to be handled. After a piece of view item
* was converted, appropriate consumable value should be {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consumed}.
diff --git a/src/conversion/upcasthelpers.js b/src/conversion/upcasthelpers.js
index f80a450a4..ad3b4250c 100644
--- a/src/conversion/upcasthelpers.js
+++ b/src/conversion/upcasthelpers.js
@@ -66,7 +66,8 @@ export default class UpcastHelpers extends ConversionHelpers {
*
* @method #elementToElement
* @param {Object} config Conversion configuration.
- * @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted.
+ * @param {module:engine/view/matcher~MatcherPattern} [config.view] Pattern matching all view elements which should be converted. If not
+ * set, the converter will fire for every view element.
* @param {String|module:engine/model/element~Element|Function} config.model Name of the model element, a model element
* instance or a function that takes a view element and returns a model element. The model element will be inserted in the model.
* @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
@@ -393,7 +394,8 @@ export function convertSelectionChange( model, mapper ) {
// See {@link ~UpcastHelpers#elementToElement `.elementToElement()` upcast helper} for examples.
//
// @param {Object} config Conversion configuration.
-// @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted.
+// @param {module:engine/view/matcher~MatcherPattern} [config.view] Pattern matching all view elements which should be converted. If not
+// set, the converter will fire for every view element.
// @param {String|module:engine/model/element~Element|Function} config.model Name of the model element, a model element
// instance or a function that takes a view element and returns a model element. The model element will be inserted in the model.
// @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
@@ -510,19 +512,26 @@ function getViewElementNameFromConfig( config ) {
// @param {Object} config Conversion configuration.
// @returns {Function} View to model converter.
function prepareToElementConverter( config ) {
- const matcher = new Matcher( config.view );
+ const matcher = config.view ? new Matcher( config.view ) : null;
return ( evt, data, conversionApi ) => {
- // This will be usually just one pattern but we support matchers with many patterns too.
- const match = matcher.match( data.viewItem );
+ let match = {};
- // If there is no match, this callback should not do anything.
- if ( !match ) {
- return;
+ // If `config.view` has not been passed do not try matching. In this case, the converter should fire for all elements.
+ if ( matcher ) {
+ // This will be usually just one pattern but we support matchers with many patterns too.
+ const matcherResult = matcher.match( data.viewItem );
+
+ // If there is no match, this callback should not do anything.
+ if ( !matcherResult ) {
+ return;
+ }
+
+ match = matcherResult.match;
}
// Force consuming element's name.
- match.match.name = true;
+ match.name = true;
// Create model element basing on config.
const modelElement = getModelElement( config.model, data.viewItem, conversionApi.writer );
@@ -533,7 +542,7 @@ function prepareToElementConverter( config ) {
}
// When element was already consumed then skip it.
- if ( !conversionApi.consumable.test( data.viewItem, match.match ) ) {
+ if ( !conversionApi.consumable.test( data.viewItem, match ) ) {
return;
}
@@ -551,32 +560,30 @@ function prepareToElementConverter( config ) {
conversionApi.writer.insert( modelElement, splitResult.position );
// Convert children and insert to element.
- const childrenResult = conversionApi.convertChildren( data.viewItem, conversionApi.writer.createPositionAt( modelElement, 0 ) );
+ conversionApi.convertChildren( data.viewItem, conversionApi.writer.createPositionAt( modelElement, 0 ) );
// Consume appropriate value from consumable values list.
- conversionApi.consumable.consume( data.viewItem, match.match );
+ conversionApi.consumable.consume( data.viewItem, match );
+
+ const parts = conversionApi.getSplitParts( modelElement );
// Set conversion result range.
data.modelRange = new ModelRange(
- // Range should start before inserted element
conversionApi.writer.createPositionBefore( modelElement ),
- // Should end after but we need to take into consideration that children could split our
- // element, so we need to move range after parent of the last converted child.
- // before: []
- // after: []
- conversionApi.writer.createPositionAfter( childrenResult.modelCursor.parent )
+ conversionApi.writer.createPositionAfter( parts[ parts.length - 1 ] )
);
- // Now we need to check where the modelCursor should be.
- // If we had to split parent to insert our element then we want to continue conversion inside split parent.
- //
- // before: []
- // after: []
+ // Now we need to check where the `modelCursor` should be.
if ( splitResult.cursorParent ) {
- data.modelCursor = conversionApi.writer.createPositionAt( splitResult.cursorParent, 0 );
+ // If we split parent to insert our element then we want to continue conversion in the new part of the split parent.
+ //
+ // before: foo[]
+ // after: foo[]
- // Otherwise just continue after inserted element.
+ data.modelCursor = conversionApi.writer.createPositionAt( splitResult.cursorParent, 0 );
} else {
+ // Otherwise just continue after inserted element.
+
data.modelCursor = data.modelRange.end;
}
};
diff --git a/src/model/writer.js b/src/model/writer.js
index 99ebdc942..182c9022d 100644
--- a/src/model/writer.js
+++ b/src/model/writer.js
@@ -717,8 +717,8 @@ export default class Writer {
* @param {module:engine/model/position~Position} position Position of split.
* @param {module:engine/model/node~Node} [limitElement] Stop splitting when this element will be reached.
* @returns {Object} result Split result.
- * @returns {module:engine/model/position~Position} result.position between split elements.
- * @returns {module:engine/model/range~Range} result.range Range that stars from the end of the first split element and ands
+ * @returns {module:engine/model/position~Position} result.position Position between split elements.
+ * @returns {module:engine/model/range~Range} result.range Range that stars from the end of the first split element and ends
* at the beginning of the first copy element.
*/
split( position, limitElement ) {
diff --git a/tests/conversion/upcastdispatcher.js b/tests/conversion/upcastdispatcher.js
index deb6e5ce7..d4138f6b5 100644
--- a/tests/conversion/upcastdispatcher.js
+++ b/tests/conversion/upcastdispatcher.js
@@ -5,6 +5,7 @@
import UpcastDispatcher from '../../src/conversion/upcastdispatcher';
import ViewContainerElement from '../../src/view/containerelement';
+import ViewElement from '../../src/view/element';
import ViewDocumentFragment from '../../src/view/documentfragment';
import ViewText from '../../src/view/text';
@@ -38,10 +39,12 @@ describe( 'UpcastDispatcher', () => {
expect( dispatcher.conversionApi ).to.have.property( 'splitToAllowedParent' ).that.is.instanceof( Function );
} );
- it( 'should have properties', () => {
+ it( 'should not crash if no additional api is passed', () => {
const dispatcher = new UpcastDispatcher();
- expect( dispatcher._removeIfEmpty ).to.instanceof( Set );
+ expect( dispatcher.conversionApi ).to.have.property( 'convertItem' ).that.is.instanceof( Function );
+ expect( dispatcher.conversionApi ).to.have.property( 'convertChildren' ).that.is.instanceof( Function );
+ expect( dispatcher.conversionApi ).to.have.property( 'splitToAllowedParent' ).that.is.instanceof( Function );
} );
} );
@@ -49,10 +52,10 @@ describe( 'UpcastDispatcher', () => {
let dispatcher;
beforeEach( () => {
- dispatcher = new UpcastDispatcher();
+ dispatcher = new UpcastDispatcher( { schema: model.schema } );
} );
- it( 'should create api for current conversion process', () => {
+ it( 'should create api for a conversion process', () => {
const viewElement = new ViewContainerElement( 'p', null, new ViewText( 'foobar' ) );
// To be sure that both converters was called.
@@ -64,13 +67,11 @@ describe( 'UpcastDispatcher', () => {
// Conversion process properties should be undefined/empty before conversion.
expect( dispatcher.conversionApi.writer ).to.not.ok;
expect( dispatcher.conversionApi.store ).to.not.ok;
- expect( dispatcher._removeIfEmpty.size ).to.equal( 0 );
dispatcher.on( 'element', ( evt, data, conversionApi ) => {
// Check conversion api params.
expect( conversionApi.writer ).to.instanceof( ModelWriter );
expect( conversionApi.store ).to.deep.equal( {} );
- expect( dispatcher._removeIfEmpty.size ).to.equal( 0 );
// Remember writer to check in next converter that is exactly the same instance (the same undo step).
writer = conversionApi.writer;
@@ -78,9 +79,6 @@ describe( 'UpcastDispatcher', () => {
// Add some data to conversion storage to verify them in next converter.
conversionApi.store.foo = 'bar';
- // Add empty element and mark as a split result to check in next converter.
- dispatcher._removeIfEmpty.add( conversionApi.writer.createElement( 'paragraph' ) );
-
// Convert children - this will call second converter.
conversionApi.convertChildren( data.viewItem, data.modelCursor );
@@ -94,9 +92,6 @@ describe( 'UpcastDispatcher', () => {
// Data set by previous converter are remembered.
expect( conversionApi.store ).to.deep.equal( { foo: 'bar' } );
- // Split element is remembered as well.
- expect( dispatcher._removeIfEmpty.size ).to.equal( 1 );
-
spy();
} );
@@ -108,7 +103,6 @@ describe( 'UpcastDispatcher', () => {
// Conversion process properties should be cleared after conversion.
expect( dispatcher.conversionApi.writer ).to.not.ok;
expect( dispatcher.conversionApi.store ).to.not.ok;
- expect( dispatcher._removeIfEmpty.size ).to.equal( 0 );
} );
it( 'should fire viewCleanup event on converted view part', () => {
@@ -248,58 +242,50 @@ describe( 'UpcastDispatcher', () => {
} );
it( 'should remove empty elements that was created as a result of split', () => {
- const viewElement = new ViewContainerElement( 'p' );
+ const viewElement = new ViewElement( 'div', null, [
+ new ViewElement( 'p', null, [
+ new ViewElement( 'img' )
+ ] )
+ ] );
- // To be sure that converter was called.
- const spy = sinon.spy();
+ model.schema.register( 'div', { allowIn: '$root' } );
+ model.schema.register( 'p', { allowIn: 'div' } );
+ model.schema.register( 'image', { allowIn: '$root' } );
- dispatcher.on( 'element', ( evt, data, conversionApi ) => {
- // First let's convert target element.
- const paragraph = conversionApi.writer.createElement( 'paragraph' );
- conversionApi.writer.insert( paragraph, data.modelCursor );
+ dispatcher.on( 'element:img', ( evt, data, conversionApi ) => {
+ const writer = conversionApi.writer;
- // Then add some elements and mark as split.
+ const modelElement = writer.createElement( 'image' );
+ const splitResult = conversionApi.splitToAllowedParent( modelElement, data.modelCursor );
+ writer.insert( modelElement, splitResult.position );
- // Create and insert empty split element before target element.
- const emptySplit = conversionApi.writer.createElement( 'paragraph' );
- conversionApi.writer.insert( emptySplit, ModelPosition._createAfter( paragraph ) );
+ data.modelRange = writer.createRangeOn( modelElement );
+ data.modelCursor = writer.createPositionAt( splitResult.cursorParent, 0 );
- // Create and insert not empty split after target element.
- const notEmptySplit = conversionApi.writer.createElement( 'paragraph' );
- conversionApi.writer.appendText( 'foo', notEmptySplit );
- conversionApi.writer.insert( notEmptySplit, ModelPosition._createAfter( emptySplit ) );
+ // Prevent below converter to fire.
+ evt.stop();
+ }, { priority: 'high' } );
- // Create and insert split with other split inside (both should be removed)
- const outerSplit = conversionApi.writer.createElement( 'paragraph' );
- const innerSplit = conversionApi.writer.createElement( 'paragraph' );
- conversionApi.writer.append( innerSplit, outerSplit );
- conversionApi.writer.insert( outerSplit, ModelPosition._createBefore( paragraph ) );
+ dispatcher.on( 'element', ( evt, data, conversionApi ) => {
+ const writer = conversionApi.writer;
- dispatcher._removeIfEmpty.add( emptySplit );
- dispatcher._removeIfEmpty.add( notEmptySplit );
- dispatcher._removeIfEmpty.add( outerSplit );
- dispatcher._removeIfEmpty.add( innerSplit );
+ const modelElement = writer.createElement( data.viewItem.name );
+ writer.insert( modelElement, data.modelCursor );
- data.modelRange = ModelRange._createOn( paragraph );
- data.modelCursor = data.modelRange.end;
+ const result = conversionApi.convertChildren( data.viewItem, writer.createPositionAt( modelElement, 0 ) );
- // We have the following result:
- //
[]foo
- // Everything out of selected range is a result of the split.
-
- spy();
+ data.modelRange = writer.createRange(
+ writer.createPositionBefore( modelElement ),
+ conversionApi.writer.createPositionAfter( result.modelCursor.parent )
+ );
+ data.modelCursor = data.modelRange.end;
} );
const result = model.change( writer => dispatcher.convert( viewElement, writer ) );
- // Empty split elements should be removed and we should have the following result:
- // []foo
- expect( result.childCount ).to.equal( 2 );
- expect( result.getChild( 0 ).name ).to.equal( 'paragraph' );
- expect( result.getChild( 0 ).childCount ).to.equal( 0 );
- expect( result.getChild( 1 ).name ).to.equal( 'paragraph' );
- expect( result.getChild( 1 ).childCount ).to.equal( 1 );
- expect( result.getChild( 1 ).getChild( 0 ).data ).to.equal( 'foo' );
+ // After splits `div` and `p` are empty so they should be removed.
+ expect( result.childCount ).to.equal( 1 );
+ expect( result.getChild( 0 ).name ).to.equal( 'image' );
} );
it( 'should extract temporary markers elements from converter element and create static markers list', () => {
@@ -628,5 +614,99 @@ describe( 'UpcastDispatcher', () => {
sinon.assert.calledOnce( spy );
} );
} );
+
+ describe( 'getSplitParts()', () => {
+ it( 'should return an array containing only passed element if the element has not been split', () => {
+ model.schema.register( 'paragraph', { allowIn: '$root' } );
+
+ const spy = sinon.spy();
+
+ dispatcher.on( 'element', ( evt, data, conversionApi ) => {
+ const modelElement = conversionApi.writer.createElement( 'paragraph' );
+ const parts = conversionApi.getSplitParts( modelElement );
+
+ expect( parts ).to.deep.equal( [ modelElement ] );
+
+ spy();
+
+ // Overwrite converters specified in `beforeEach`.
+ evt.stop();
+ }, { priority: 'high' } );
+
+ const viewElement = new ViewElement( 'p' );
+
+ model.change( writer => dispatcher.convert( viewElement, writer ) );
+
+ expect( spy.called ).to.be.true;
+ } );
+
+ it( 'should return all parts of the split element', () => {
+ model.schema.register( 'paragraph', { allowIn: '$root' } );
+ model.schema.register( 'text', { allowIn: 'paragraph' } );
+ model.schema.register( 'image', { allowIn: '$root' } );
+
+ dispatcher.on( 'text', ( evt, data, conversionApi ) => {
+ const modelText = conversionApi.writer.createText( data.viewItem.data );
+
+ conversionApi.writer.insert( modelText, data.modelCursor );
+
+ data.modelRange = conversionApi.writer.createRangeOn( modelText );
+ data.modelCursor = data.modelRange.end;
+
+ // Overwrite converters specified in `beforeEach`.
+ evt.stop();
+ }, { priority: 'high' } );
+
+ dispatcher.on( 'element:image', ( evt, data, conversionApi ) => {
+ const modelElement = conversionApi.writer.createElement( 'image' );
+
+ const splitResult = conversionApi.splitToAllowedParent( modelElement, data.modelCursor );
+
+ conversionApi.writer.insert( modelElement, splitResult.position );
+
+ data.modelRange = conversionApi.writer.createRangeOn( modelElement );
+ data.modelCursor = conversionApi.writer.createPositionAt( splitResult.cursorParent, 0 );
+
+ // Overwrite converters specified in `beforeEach`.
+ evt.stop();
+ }, { priority: 'high' } );
+
+ const spy = sinon.spy();
+
+ dispatcher.on( 'element:p', ( evt, data, conversionApi ) => {
+ const modelElement = conversionApi.writer.createElement( 'paragraph' );
+
+ conversionApi.writer.insert( modelElement, data.modelCursor );
+ conversionApi.convertChildren( data.viewItem, conversionApi.writer.createPositionAt( modelElement, 0 ) );
+
+ const parts = conversionApi.getSplitParts( modelElement );
+
+ expect( parts.length ).to.equal( 3 );
+
+ expect( parts[ 0 ].getChild( 0 ).data ).to.equal( 'foo' );
+ expect( parts[ 1 ].getChild( 0 ).data ).to.equal( 'bar' );
+ expect( parts[ 2 ].getChild( 0 ).data ).to.equal( 'xyz' );
+
+ expect( parts[ 0 ] ).to.equal( modelElement );
+
+ spy();
+
+ // Overwrite converters specified in `beforeEach`.
+ evt.stop();
+ }, { priority: 'high' } );
+
+ const viewElement = new ViewElement( 'p', null, [
+ new ViewText( 'foo' ),
+ new ViewElement( 'image' ),
+ new ViewText( 'bar' ),
+ new ViewElement( 'image' ),
+ new ViewText( 'xyz' )
+ ] );
+
+ model.change( writer => dispatcher.convert( viewElement, writer, [ '$root' ] ) );
+
+ expect( spy.called ).to.be.true;
+ } );
+ } );
} );
} );
diff --git a/tests/conversion/upcasthelpers.js b/tests/conversion/upcasthelpers.js
index 99cb62254..c6e5f01b6 100644
--- a/tests/conversion/upcasthelpers.js
+++ b/tests/conversion/upcasthelpers.js
@@ -112,6 +112,15 @@ describe( 'UpcastHelpers', () => {
expectResult( new ViewContainerElement( 'p', { 'data-level': 2 } ), '' );
} );
+ it( 'config.view is not set - should fire conversion for every element', () => {
+ upcastHelpers.elementToElement( {
+ model: 'paragraph'
+ } );
+
+ expectResult( new ViewContainerElement( 'p' ), '' );
+ expectResult( new ViewContainerElement( 'foo' ), '' );
+ } );
+
it( 'should fire conversion of the element children', () => {
upcastHelpers.elementToElement( { view: 'p', model: 'paragraph' } );