/** * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /* global document */ import CodeBlockEditing from '../src/codeblockediting'; import CodeBlockCommand from '../src/codeblockcommand'; import AlignmentEditing from '@ckeditor/ckeditor5-alignment/src/alignmentediting'; import BoldEditing from '@ckeditor/ckeditor5-basic-styles/src/bold/boldediting'; import Enter from '@ckeditor/ckeditor5-enter/src/enter'; import ShiftEnter from '@ckeditor/ckeditor5-enter/src/shiftenter'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Undo from '@ckeditor/ckeditor5-undo/src/undo'; import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata'; import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; import { _clear as clearTranslations, add as addTranslations } from '@ckeditor/ckeditor5-utils/src/translation-service'; describe( 'CodeBlockEditing', () => { let editor, element, model, view, viewDoc; before( () => { addTranslations( 'en', { 'Plain text': 'Plain text' } ); addTranslations( 'pl', { 'Plain text': 'Zwykły tekst' } ); } ); after( () => { clearTranslations(); } ); beforeEach( () => { element = document.createElement( 'div' ); document.body.appendChild( element ); return ClassicTestEditor .create( element, { language: 'en', plugins: [ CodeBlockEditing, AlignmentEditing, BoldEditing, Enter, Paragraph, Undo ] } ) .then( newEditor => { editor = newEditor; model = editor.model; view = editor.editing.view; viewDoc = view.document; } ); } ); afterEach( () => { return editor.destroy().then( () => element.remove() ); } ); it( 'defines plugin name', () => { expect( CodeBlockEditing.pluginName ).to.equal( 'CodeBlockEditing' ); } ); it( 'defines plugin dependencies', () => { expect( CodeBlockEditing.requires ).to.have.members( [ ShiftEnter ] ); } ); describe( 'config', () => { describe( 'languages', () => { describe( 'default value', () => { it( 'should be set', () => { expect( editor.config.get( 'codeBlock.languages' ) ).to.deep.equal( [ { class: 'plaintext', label: 'Plain text' }, { class: 'c', label: 'C' }, { class: 'cs', label: 'C#' }, { class: 'cpp', label: 'C++' }, { class: 'css', label: 'CSS' }, { class: 'diff', label: 'Diff' }, { class: 'xml', label: 'HTML/XML' }, { class: 'java', label: 'Java' }, { class: 'javascript', label: 'JavaScript' }, { class: 'php', label: 'PHP' }, { class: 'python', label: 'Python' }, { class: 'ruby', label: 'Ruby' }, { class: 'typescript', label: 'TypeScript' } ] ); } ); } ); it( 'should be recognized when loading data', () => { return ClassicTestEditor.create( '
bar
' + '
baz
', { plugins: [ CodeBlockEditing ], codeBlock: { languages: [ { class: 'foo', label: 'Foo' }, { class: 'bar', label: 'Bar' }, ] } } ) .then( editor => { model = editor.model; expect( getModelData( model ) ).to.equal( '[]bar' + 'baz' ); return editor.destroy(); } ); } ); it( 'should use the first if the code in data has no language', () => { return ClassicTestEditor .create( '
bar
', { plugins: [ CodeBlockEditing ], codeBlock: { languages: [ { class: 'foo', label: 'Foo' }, { class: 'bar', label: 'Bar' } ] } } ) .then( editor => { model = editor.model; expect( getModelData( model ) ).to.equal( '[]bar' ); return editor.destroy(); } ); } ); it( 'should use the first if the code in data has an invalid language', () => { return ClassicTestEditor .create( '
bar
', { plugins: [ CodeBlockEditing ], codeBlock: { languages: [ { class: 'foo', label: 'Foo' }, { class: 'bar', label: 'Bar' } ] } } ) .then( editor => { model = editor.model; expect( getModelData( model ) ).to.equal( '[]bar' ); return editor.destroy(); } ); } ); } ); } ); it( 'adds a codeBlock command', () => { expect( editor.commands.get( 'codeBlock' ) ).to.be.instanceOf( CodeBlockCommand ); } ); it( 'allows for codeBlock in the $root', () => { expect( model.schema.checkChild( [ '$root' ], 'codeBlock' ) ).to.be.true; } ); it( 'allows only for $text in codeBlock', () => { expect( model.schema.checkChild( [ '$root', 'codeBlock' ], '$text' ) ).to.equal( true ); expect( model.schema.checkChild( [ '$root', 'codeBlock' ], '$block' ) ).to.equal( false ); expect( model.schema.checkChild( [ '$root', 'codeBlock' ], 'codeBlock' ) ).to.equal( false ); } ); it( 'disallows all attributes (except "language") for codeBlock', () => { setModelData( model, 'f[o]o' ); editor.execute( 'alignment', { value: 'right' } ); editor.execute( 'bold' ); expect( getModelData( model ) ).to.equal( 'f[o]o' ); } ); describe( 'enter key handling', () => { it( 'should force shiftEnter command when pressing enter inside a codeBlock', () => { const enterCommand = editor.commands.get( 'enter' ); const shiftEnterCommand = editor.commands.get( 'shiftEnter' ); sinon.spy( enterCommand, 'execute' ); sinon.spy( shiftEnterCommand, 'execute' ); setModelData( model, 'foo[]bar' ); viewDoc.fire( 'enter', new DomEventData( viewDoc, { preventDefault: sinon.spy() } ) ); expect( getModelData( model ) ).to.equal( 'foo[]bar' ); sinon.assert.calledOnce( shiftEnterCommand.execute ); sinon.assert.notCalled( enterCommand.execute ); } ); it( 'should execute enter command when pressing enter out of codeBlock', () => { const enterCommand = editor.commands.get( 'enter' ); const shiftEnterCommand = editor.commands.get( 'shiftEnter' ); sinon.spy( enterCommand, 'execute' ); sinon.spy( shiftEnterCommand, 'execute' ); setModelData( model, 'foo[]bar' ); viewDoc.fire( 'enter', new DomEventData( viewDoc, { preventDefault: sinon.spy() } ) ); expect( getModelData( model ) ).to.equal( 'foo[]bar' ); sinon.assert.calledOnce( enterCommand.execute ); sinon.assert.notCalled( shiftEnterCommand.execute ); } ); it( 'should leave the block when pressed twice at the end', () => { const spy = sinon.spy( editor.editing.view, 'scrollToTheSelection' ); setModelData( model, 'foo[]' ); viewDoc.fire( 'enter', new DomEventData( viewDoc, { preventDefault: sinon.spy() } ) ); expect( getModelData( model ) ).to.equal( 'foo[]' ); viewDoc.fire( 'enter', new DomEventData( viewDoc, { preventDefault: sinon.spy() } ) ); expect( getModelData( model ) ).to.equal( 'foo' + '[]' ); sinon.assert.calledOnce( spy ); editor.execute( 'undo' ); expect( getModelData( model ) ).to.equal( 'foo[]' ); editor.execute( 'undo' ); expect( getModelData( model ) ).to.equal( 'foo[]' ); } ); it( 'should not leave the block when pressed twice when in the middle of the code', () => { setModelData( model, 'fo[]o' ); viewDoc.fire( 'enter', new DomEventData( viewDoc, { preventDefault: sinon.spy() } ) ); expect( getModelData( model ) ).to.equal( 'fo[]o' ); viewDoc.fire( 'enter', new DomEventData( viewDoc, { preventDefault: sinon.spy() } ) ); expect( getModelData( model ) ).to.equal( 'fo[]o' ); } ); it( 'should not leave the block when pressed twice at the beginning of the code', () => { setModelData( model, '[]foo' ); viewDoc.fire( 'enter', new DomEventData( viewDoc, { preventDefault: sinon.spy() } ) ); expect( getModelData( model ) ).to.equal( '[]foo' ); viewDoc.fire( 'enter', new DomEventData( viewDoc, { preventDefault: sinon.spy() } ) ); expect( getModelData( model ) ).to.equal( '[]foo' ); } ); it( 'should not leave the block when pressed shift+enter twice at the end of the code', () => { setModelData( model, 'foo[]' ); viewDoc.fire( 'enter', new DomEventData( viewDoc, { preventDefault: sinon.spy() }, { isSoft: true } ) ); expect( getModelData( model ) ).to.equal( 'foo[]' ); } ); } ); describe( 'editing pipeline m -> v', () => { it( 'should convert empty codeBlock to empty pre tag', () => { setModelData( model, '' ); expect( getViewData( view ) ).to.equal( '
[]
' ); } ); it( 'should convert non-empty codeBlock to pre tag', () => { setModelData( model, 'Foo' ); expect( getViewData( view ) ).to.equal( '
{}Foo
' ); } ); it( 'should convert codeBlock with softBreaks to pre tag #1', () => { setModelData( model, '' + 'Foo' + 'Bar' + 'Biz' + '' ); expect( getViewData( view ) ).to.equal( '
' +
					'{}Foo

Bar

Biz
' + '
' ); } ); it( 'should convert codeBlock with softBreaks to pre tag #2', () => { setModelData( model, '' + '' + '' + 'Foo' + '' + '' + '' ); expect( getViewData( view ) ).to.equal( '
' +
					'[]



Foo



' + '
' ); } ); it( 'should use localized "Plain text" label', () => { const element = document.createElement( 'div' ); document.body.appendChild( element ); return ClassicTestEditor .create( element, { language: 'pl', plugins: [ CodeBlockEditing, AlignmentEditing, BoldEditing, Enter, Paragraph ] } ) .then( newEditor => { const editor = newEditor; const model = editor.model; const view = editor.editing.view; setModelData( model, 'foo' ); expect( getViewData( view ) ).to.equal( '
' +
							'{}foo' +
						'
' ); element.remove(); return editor.destroy(); } ); } ); } ); describe( 'data pipeline m -> v conversion ', () => { it( 'should convert empty codeBlock to empty pre tag', () => { setModelData( model, '' ); expect( editor.getData( { trim: 'none' } ) ).to.equal( '
 
' ); } ); it( 'should convert non-empty codeBlock to pre tag', () => { setModelData( model, 'Foo' ); expect( editor.getData() ).to.equal( '
Foo
' ); } ); it( 'should convert codeBlock with softBreaks to pre tag #1', () => { setModelData( model, '' + 'Foo' + 'Bar' + 'Biz' + '' + 'AB' ); expect( editor.getData() ).to.equal( '
Foo\nBar\nBiz

A
B

' ); } ); it( 'should convert codeBlock with softBreaks to pre tag #2', () => { setModelData( model, '' + '' + '' + 'Foo' + '' + '' + '' ); expect( editor.getData() ).to.equal( '
\n\nFoo\n\n
' ); } ); it( 'should convert codeBlock with html content', () => { setModelData( model, '[]' ); model.change( writer => writer.insertText( '

Foo

', model.document.selection.getFirstPosition() ) ); expect( editor.getData() ).to.equal( '
' +
					'<div><p>Foo</p></div>' +
				'
' ); } ); it( 'should be overridable', () => { editor.data.downcastDispatcher.on( 'insert:codeBlock', ( evt, data, api ) => { const targetViewPosition = api.mapper.toViewPosition( model.createPositionBefore( data.item ) ); const code = api.writer.createContainerElement( 'code' ); api.consumable.consume( data.item, 'insert' ); api.writer.insert( targetViewPosition, code ); api.mapper.bindElements( data.item, code ); }, { priority: 'high' } ); editor.data.downcastDispatcher.on( 'insert:softBreak', ( evt, data, api ) => { const position = api.mapper.toViewPosition( model.createPositionBefore( data.item ) ); api.consumable.consume( data.item, 'insert' ); api.writer.insert( position, api.writer.createText( '\n' ) ); }, { priority: 'highest' } ); setModelData( model, 'FooBar' ); expect( editor.getData() ).to.equal( 'Foo\nBar' ); } ); } ); describe( 'data pipeline v -> m conversion ', () => { it( 'should not convert empty pre tag to code block', () => { editor.setData( '
' );

			expect( getModelData( model ) ).to.equal( '[]' );
		} );

		it( 'should not convert pre with no code child to code block', () => {
			editor.setData( '
' ); expect( getModelData( model ) ).to.equal( '[]' ); } ); it( 'should convert pre > code to code block', () => { editor.setData( '
' ); expect( getModelData( model ) ).to.equal( '[]' ); } ); it( 'should convert pre > code with multi-line text to code block #1', () => { editor.setData( '
foo\nbar
' ); expect( getModelData( model ) ).to.equal( '[]' + 'foo' + '' + 'bar' + '' ); } ); it( 'should convert pre > code with multi-line text to code block #2', () => { editor.setData( '
\n\nfoo\n\n
' ); expect( getModelData( model ) ).to.equal( '[]' + '' + '' + 'foo' + '' + '' + '' ); } ); it( 'should convert pre > code with HTML inside', () => { editor.setData( '

Foo

\n

Bar

' ); expect( getModelData( model ) ).to.equal( '[]' + '

Foo

' + '' + '

Bar

' + '
' ); } ); it( 'should convert pre > code tag with HTML and nested pre > code tag', () => { editor.setData( '

Foo

Bar

Biz

' ); expect( getModelData( model ) ).to.equal( '[]

Foo

Bar

Biz

' ); } ); it( 'should convert pre > code tag with escaped html content', () => { editor.setData( '
<div><p>Foo</p></div>
' ); expect( getModelData( model ) ).to.equal( '[]

Foo

' ); } ); it( 'should be overridable', () => { editor.data.upcastDispatcher.on( 'element:pre', ( evt, data, api ) => { const modelItem = api.writer.createElement( 'codeBlock' ); api.writer.appendText( 'Hello World!', modelItem ); api.writer.insert( modelItem, data.modelCursor ); api.consumable.consume( data.viewItem, { name: true } ); data.modelCursor = api.writer.createPositionAfter( modelItem ); data.modelRange = api.writer.createRangeOn( modelItem ); }, { priority: 'high' } ); editor.setData( '
Foo Bar
' ); expect( getModelData( model ) ).to.equal( '[]Hello World!' ); } ); it( 'should split parents to correctly upcast the code block', () => { editor.setData( '

foo

x
bar

' ); // Note: The empty should not be here. It's a conversion/auto–paragraphing bug. expect( getModelData( model ) ).to.equal( '[]foo' + 'x' + 'bar' + '' ); } ); it( 'should upcast two code blocks in a row (#1)', () => { editor.setData( '
foo
bar
' ); expect( getModelData( model ) ).to.equal( '[]foobar' ); } ); it( 'should upcast two code blocks in a row (#2)', () => { editor.setData( `
foo
bar
` ); // Note: The empty in between should not be here. It's a conversion/auto–paragraphing bug. expect( getModelData( model ) ).to.equal( '[]foo' + ' ' + 'bar' ); } ); it( 'should not convert when modelCursor and its ancestors disallow to insert codeBlock', () => { model.document.createRoot( '$title', 'title' ); model.schema.register( '$title', { disallow: '$block', allow: 'inline' } ); editor.data.set( { title: '
foo
' } ); expect( getModelData( model, { rootName: 'title', withoutSelection: true } ) ).to.equal( '' ); } ); } ); describe( 'clipboard integration', () => { it( 'should not intercept input when selection anchored outside any code block', () => { setModelData( model, 'f[]oo' ); const dataTransferMock = { getData: sinon.stub().withArgs( 'text/plain' ).returns( 'bar' ) }; viewDoc.fire( 'clipboardInput', { dataTransfer: dataTransferMock, stop: sinon.spy() } ); expect( getModelData( model ) ).to.equal( 'f[]oo' ); sinon.assert.notCalled( dataTransferMock.getData ); } ); it( 'should intercept input when selection anchored in the code block', () => { setModelData( model, 'f[o]o' ); const dataTransferMock = { getData: sinon.stub().withArgs( 'text/plain' ).returns( 'bar\nbaz\n' ) }; viewDoc.fire( 'clipboardInput', { dataTransfer: dataTransferMock, stop: sinon.spy() } ); expect( getModelData( model ) ).to.equal( '' + 'fbar' + '' + 'baz' + '' + '[]o' + '' ); sinon.assert.calledOnce( dataTransferMock.getData ); } ); } ); } );