Преглед изворни кода

Moved extracting of marker stamps to ViewConversiondispatcher.

Oskar Wróbel пре 9 година
родитељ
комит
1e57f0207b

+ 75 - 1
packages/ckeditor5-engine/src/conversion/viewconversiondispatcher.js

@@ -11,6 +11,12 @@ import ViewConsumable from './viewconsumable';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
+import ModelRange from '../model/range';
+import ModelPosition from '../model/position';
+import ModelTreeWalker from '../model/treewalker';
+import ModelNode from '../model/node';
+import ModelDocumentFragment from '../model/documentfragment';
+import { remove } from '../model/writer';
 
 /**
  * `ViewConversionDispatcher` is a central point of {@link module:engine/view/view view} conversion, which is a process of
@@ -135,7 +141,18 @@ export default class ViewConversionDispatcher {
 
 		const consumable = ViewConsumable.createFrom( viewItem );
 
-		return this._convertItem( viewItem, consumable, additionalData );
+		const conversionResult = this._convertItem( viewItem, consumable, additionalData );
+
+		if ( conversionResult instanceof ModelNode || conversionResult instanceof ModelDocumentFragment ) {
+			const { modelItem, markersData } = extractMarkersFromModelFragment( conversionResult );
+
+			return {
+				conversionResult: modelItem,
+				markersData: markersData
+			};
+		}
+
+		return { conversionResult, markersData: new Map() };
 	}
 
 	/**
@@ -221,6 +238,63 @@ export default class ViewConversionDispatcher {
 
 mix( ViewConversionDispatcher, EmitterMixin );
 
+// Traverses given model item and searches elements which marks marker range. Found element is removed from
+// DocumentFragment but path of this element is stored in a Map which is then returned.
+//
+// @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/node~Node} modelItem Fragment of model.
+// @returns {Object} Object with cleaned up model fragment and Map with markers data.
+function extractMarkersFromModelFragment( modelItem ) {
+	const markerStamps = new Set();
+	const markersData = new Map();
+
+	if ( modelItem.is( 'text' ) ) {
+		return { modelItem, markersData };
+	}
+
+	// Create ModelTreeWalker.
+	const walker = new ModelTreeWalker( {
+		startPosition: ModelPosition.createAt( modelItem, 0 ),
+		ignoreElementEnd: true,
+		shallow: false
+	} );
+
+	// Walk through DocumentFragment and collect marker elements.
+	for ( const value of walker ) {
+		// Check if current element is a marker stamp.
+		if ( value.item.name == '$marker' ) {
+			markerStamps.add( value.item );
+		}
+	}
+
+	// Walk through collected marker elements store its path and remove its from the DocumentFragment.
+	for ( const stamp of markerStamps ) {
+		const markerName = stamp.getAttribute( 'data-name' );
+		const currentPosition = ModelPosition.createBefore( stamp );
+
+		// When marker of given name is not stored it means that we have found the beginning of the range.
+		if ( !markersData.has( markerName ) ) {
+			markersData.set( markerName, { startPath: currentPosition.path } );
+			// Otherwise is means that we have found end of the marker range.
+		} else {
+			markersData.get( markerName ).endPath = currentPosition.path;
+		}
+
+		// Remove marker stamp element from DocumentFragment.
+		remove( ModelRange.createOn( stamp ) );
+	}
+
+	return { modelItem, markersData };
+}
+
+/**
+ * Model data that is a result of the conversion process.
+ *
+ * @typedef {Object} engine/conversion/viewconversiondispatcher~ConvertedModelDocument
+ * @property {module:engine/model/documentfragment~DocumentFragment|module:engine/model/element~Element|
+ * module:engine/model/text~Text} modelItem Model document item.
+ * @property {Map} markersData Map with markers data in format [ 'markerName', { startPath: [ 1, 1 ], endPath: [ 1, 4 ] } ]
+ */
+
 /**
  * Conversion interface that is registered for given {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher}
  * and is passed as one of parameters when {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher dispatcher}

+ 72 - 11
packages/ckeditor5-engine/tests/conversion/viewconversiondispatcher.js

@@ -9,6 +9,11 @@ import ViewAttributeElement from '../../src/view/attributeelement';
 import ViewDocumentFragment from '../../src/view/documentfragment';
 import ViewText from '../../src/view/text';
 
+import ModelSchema from '../../src/model/schema';
+import buildViewConverter from '../../src/conversion/buildviewconverter';
+import { convertText } from '../../src/conversion/view-to-model-converters';
+import { stringify } from '../../src/dev-utils/model';
+
 describe( 'ViewConversionDispatcher', () => {
 	describe( 'constructor()', () => {
 		it( 'should create ViewConversionDispatcher with passed api', () => {
@@ -76,10 +81,10 @@ describe( 'ViewConversionDispatcher', () => {
 			} );
 
 			// Use `additionalData` parameter to check if it was passed to the event.
-			const result = dispatcher.convert( viewText, { foo: 'bar' } );
+			const { conversionResult } = dispatcher.convert( viewText, { foo: 'bar' } );
 
 			// Check conversion result.
-			expect( result ).to.deep.equal( {
+			expect( conversionResult ).to.deep.equal( {
 				eventName: 'text',
 				input: viewText,
 				foo: 'bar'
@@ -110,10 +115,10 @@ describe( 'ViewConversionDispatcher', () => {
 			} );
 
 			// Use `additionalData` parameter to check if it was passed to the event.
-			const result = dispatcher.convert( viewElement, { foo: 'bar' } );
+			const { conversionResult } = dispatcher.convert( viewElement, { foo: 'bar' } );
 
 			// Check conversion result.
-			expect( result ).to.deep.equal( {
+			expect( conversionResult ).to.deep.equal( {
 				eventName: 'element:p',
 				input: viewElement,
 				foo: 'bar'
@@ -143,15 +148,71 @@ describe( 'ViewConversionDispatcher', () => {
 			} );
 
 			// Use `additionalData` parameter to check if it was passed to the event.
-			const result = dispatcher.convert( viewFragment, { foo: 'bar' } );
+			const { conversionResult } = dispatcher.convert( viewFragment, { foo: 'bar' } );
 
 			// Check conversion result.
-			expect( result ).to.deep.equal( {
+			expect( conversionResult ).to.deep.equal( {
 				eventName: 'documentFragment',
 				input: viewFragment,
 				foo: 'bar'
 			} );
 		} );
+
+		it( 'should convert markers stamps', () => {
+			const schema = new ModelSchema();
+			schema.registerItem( 'paragraph', '$block' );
+
+			dispatcher = new ViewConversionDispatcher( { schema } );
+			dispatcher.on( 'text', convertText() );
+
+			buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
+			buildViewConverter().for( dispatcher ).fromElement( 'm' ).toMarker();
+
+			const viewElement = new ViewContainerElement( 'p', null, [
+				new ViewText( 'F' ),
+				new ViewAttributeElement( 'm', { 'data-name': 'comment' } ),
+				new ViewText( 'o' ),
+				new ViewAttributeElement( 'm', { 'data-name': 'search' } ),
+				new ViewText( 'o ba' ),
+				new ViewAttributeElement( 'm', { 'data-name': 'comment' } ),
+				new ViewText( 'r bi' ),
+				new ViewAttributeElement( 'm', { 'data-name': 'search' } ),
+				new ViewText( 'z' )
+			] );
+
+			const { conversionResult, markersData } = dispatcher.convert( viewElement, { context: [ '$root' ] } );
+
+			expect( stringify( conversionResult ) ).to.equal( '<paragraph>Foo bar biz</paragraph>' );
+			expect( markersData.size ).to.equal( 2 );
+			expect( markersData.get( 'comment' ) ).deep.equal( { startPath: [ 1 ], endPath: [ 6 ] } );
+			expect( markersData.get( 'search' ) ).deep.equal( { startPath: [ 2 ], endPath: [ 10 ] } );
+		} );
+
+		it( 'should convert markers stamps for collapsed ranges', () => {
+			const schema = new ModelSchema();
+			schema.registerItem( 'paragraph', '$block' );
+
+			dispatcher = new ViewConversionDispatcher( { schema } );
+			dispatcher.on( 'text', convertText() );
+
+			buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' );
+			buildViewConverter().for( dispatcher ).fromElement( 'm' ).toMarker();
+
+			const viewElement = new ViewContainerElement( 'p', null, [
+				new ViewText( 'F' ),
+				new ViewAttributeElement( 'm', { 'data-name': 'comment' } ),
+				new ViewText( 'o' ),
+				new ViewAttributeElement( 'm', { 'data-name': 'search' } ),
+				new ViewText( 'o' ),
+			] );
+
+			const { conversionResult, markersData } = dispatcher.convert( viewElement, { context: [ '$root' ] } );
+
+			expect( stringify( conversionResult ) ).to.equal( '<paragraph>Foo</paragraph>' );
+			expect( markersData.size ).to.equal( 2 );
+			expect( markersData.get( 'comment' ) ).deep.equal( { startPath: [ 1 ] } );
+			expect( markersData.get( 'search' ) ).deep.equal( { startPath: [ 2 ] } );
+		} );
 	} );
 
 	describe( 'conversionApi#convertItem', () => {
@@ -177,9 +238,9 @@ describe( 'ViewConversionDispatcher', () => {
 				}
 			} );
 
-			const result = dispatcher.convert( viewFragment );
+			const { conversionResult } = dispatcher.convert( viewFragment );
 
-			expect( result ).to.deep.equal( [
+			expect( conversionResult ).to.deep.equal( [
 				{ name: 'p' },
 				{ text: 'foobar' }
 			] );
@@ -205,9 +266,9 @@ describe( 'ViewConversionDispatcher', () => {
 				data.output = conversionApi.convertChildren( data.input );
 			} );
 
-			const result = dispatcher.convert( viewFragment );
+			const { conversionResult } = dispatcher.convert( viewFragment );
 
-			expect( result ).to.deep.equal( [
+			expect( conversionResult ).to.deep.equal( [
 				{ name: 'p' },
 				{ text: 'foobar' }
 			] );
@@ -242,7 +303,7 @@ describe( 'ViewConversionDispatcher', () => {
 				] )
 			] );
 
-			expect( dispatcher.convert( viewStructure ) ).to.deep.equal( [ 'foo', ' bar ', 'xyz', 'aaa ', 'bbb', ' ', 'ccc' ] );
+			expect( dispatcher.convert( viewStructure ).conversionResult ).to.deep.equal( [ 'foo', ' bar ', 'xyz', 'aaa ', 'bbb', ' ', 'ccc' ] );
 		} );
 	} );
 } );