Преглед на файлове

Improved data conversion to properly handle HTML inside a pre tag.

Oskar Wróbel преди 6 години
родител
ревизия
4f4906e1ac
променени са 2 файла, в които са добавени 155 реда и са изтрити 1 реда
  1. 61 1
      packages/ckeditor5-code-block/src/codeblockediting.js
  2. 94 0
      packages/ckeditor5-code-block/tests/codeblockediting.js

+ 61 - 1
packages/ckeditor5-code-block/src/codeblockediting.js

@@ -61,7 +61,11 @@ export default class CodeBlockEditing extends Plugin {
 		} );
 
 		// Conversion.
-		editor.conversion.elementToElement( { model: 'codeBlock', view: 'pre' } );
+		editor.conversion.for( 'editingDowncast' ).elementToElement( { model: 'codeBlock', view: 'pre' } );
+
+		editor.conversion.for( 'dataDowncast' ).elementToElement( { model: 'codeBlock', view: 'pre' } );
+		editor.data.downcastDispatcher.on( 'insert:softBreak', dataModelViewSoftBreakInsertion( editor.model ), { priority: 'high' } );
+		editor.data.upcastDispatcher.on( 'element:pre', dataViewModelPreInsertion( editor.data ) );
 	}
 
 	/**
@@ -83,3 +87,59 @@ export default class CodeBlockEditing extends Plugin {
 		} );
 	}
 }
+
+// A model-to-view converter for the new line separator.
+//
+// @param {module:engine/model/model~Model} model
+// @returns {Function} Returns a conversion callback.
+function dataModelViewSoftBreakInsertion( model ) {
+	return ( evt, data, conversionApi ) => {
+		if ( data.item.parent.name !== 'codeBlock' ) {
+			return;
+		}
+
+		const { writer, mapper, consumable } = conversionApi;
+
+		if ( !consumable.consume( data.item, 'insert' ) ) {
+			return;
+		}
+
+		const position = mapper.toViewPosition( model.createPositionBefore( data.item ) );
+
+		writer.insert( position, writer.createText( '\n' ) );
+	};
+}
+
+// A view-to-model converter for `pre` tag.
+//
+// @param {module:engine/controller/datacontroller~DataController} dataController
+// @returns {Function} Returns a conversion callback.
+function dataViewModelPreInsertion( dataController ) {
+	return ( evt, data, conversionApi ) => {
+		const { consumable, writer } = conversionApi;
+
+		if ( !consumable.consume( data.viewItem, { name: true } ) ) {
+			return;
+		}
+
+		const modelItem = writer.createElement( 'codeBlock' );
+
+		const stringifiedElement = dataController.processor.toData( data.viewItem );
+		const textData = new RegExp( /^<pre>(.*)<\/pre>$/, 's' ).exec( stringifiedElement )[ 1 ];
+		const textLines = textData.split( '\n' ).map( data => writer.createText( data ) );
+		const lastLine = textLines[ textLines.length - 1 ];
+
+		for ( const node of textLines ) {
+			writer.append( node, modelItem );
+
+			if ( node !== lastLine ) {
+				writer.appendElement( 'softBreak', modelItem );
+			}
+		}
+
+		writer.insert( modelItem, data.modelCursor );
+
+		data.modelCursor = writer.createPositionAfter( modelItem );
+		data.modelRange = writer.createRange( data.modelCursor );
+	};
+}

+ 94 - 0
packages/ckeditor5-code-block/tests/codeblockediting.js

@@ -119,4 +119,98 @@ describe( 'CodeBlockEditing', () => {
 		sinon.assert.calledOnce( enterCommand.execute );
 		sinon.assert.notCalled( shiftEnterCommand.execute );
 	} );
+
+	describe( 'data pipeline m -> v conversion ', () => {
+		it( 'should convert empty codeBlock to empty pre tag', () => {
+			setModelData( model, '<codeBlock></codeBlock>' );
+
+			expect( editor.getData( { trim: 'none' } ) ).to.equal( '<pre>&nbsp;</pre>' );
+		} );
+
+		it( 'should convert non-empty codeBlock to pre tag', () => {
+			setModelData( model, '<codeBlock>Foo</codeBlock>' );
+
+			expect( editor.getData() ).to.equal( '<pre>Foo</pre>' );
+		} );
+
+		it( 'should convert codeBlock with softBreaks to pre tag #1', () => {
+			setModelData( model,
+				'<codeBlock>' +
+					'Foo<softBreak></softBreak>' +
+					'Bar<softBreak></softBreak>' +
+					'Biz' +
+				'</codeBlock>'
+			);
+
+			expect( editor.getData() ).to.equal( '<pre>Foo\nBar\nBiz</pre>' );
+		} );
+
+		it( 'should convert codeBlock with softBreaks to pre tag #2', () => {
+			setModelData( model,
+				'<codeBlock>' +
+					'<softBreak></softBreak>' +
+					'<softBreak></softBreak>' +
+					'Foo' +
+					'<softBreak></softBreak>' +
+					'<softBreak></softBreak>' +
+				'</codeBlock>'
+			);
+
+			expect( editor.getData() ).to.equal( '<pre>\n\nFoo\n\n</pre>' );
+		} );
+	} );
+
+	describe( 'data pipeline v -> m conversion ', () => {
+		it( 'should convert empty pre tag to code block', () => {
+			editor.setData( '<pre></pre>' );
+
+			expect( getModelData( model ) ).to.equal( '<codeBlock>[]</codeBlock>' );
+		} );
+
+		it( 'should convert pre tag with multi-line text to code block #1', () => {
+			editor.setData( '<pre>foo\nbar</pre>' );
+
+			expect( getModelData( model ) ).to.equal(
+				'<codeBlock>[]' +
+					'foo' +
+					'<softBreak></softBreak>' +
+					'bar' +
+				'</codeBlock>'
+			);
+		} );
+
+		it( 'should convert pre tag with multi-line text to code block #2', () => {
+			editor.setData( '<pre>\n\n\nfoo\n\n</pre>' );
+
+			// The result may be unclear. 3 new line characters are converted to 2 softBreak elements.
+			// That's because of HTML rule, leading new line character for <pre/> element is stripped by the DOMParser.
+			expect( getModelData( model ) ).to.equal(
+				'<codeBlock>[]' +
+					'<softBreak></softBreak>' +
+					'<softBreak></softBreak>' +
+					'foo' +
+					'<softBreak></softBreak>' +
+					'<softBreak></softBreak>' +
+				'</codeBlock>'
+			);
+		} );
+
+		it( 'should convert pre tag with HTML inside', () => {
+			editor.setData( '<pre><p>Foo</p>\n<p>Bar</p></pre>' );
+
+			expect( getModelData( model ) ).to.equal(
+				'<codeBlock>[]' +
+					'<p>Foo</p>' +
+					'<softBreak></softBreak>' +
+					'<p>Bar</p>' +
+				'</codeBlock>'
+			);
+		} );
+
+		it( 'should convert pre tag with HTML and nested pre tag', () => {
+			editor.setData( '<pre><p>Foo</p><pre>Bar</pre><p>Biz</p></pre>' );
+
+			expect( getModelData( model ) ).to.equal( '<codeBlock>[]<p>Foo</p><pre>Bar</pre><p>Biz</p></codeBlock>' );
+		} );
+	} );
 } );