/**
* @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, stringify } 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', getEvent() );
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', getEvent() );
expect( getModelData( model ) ).to.equal( 'foo[]bar' );
sinon.assert.calledOnce( enterCommand.execute );
sinon.assert.notCalled( shiftEnterCommand.execute );
} );
describe( 'indentation retention', () => {
it( 'should work when indentation is with spaces', () => {
setModelData( model, 'foo[]' );
model.change( writer => {
// foo[]
writer.insertText( ' ', model.document.getRoot().getChild( 0 ), 0 );
} );
viewDoc.fire( 'enter', getEvent() );
expect( getModelData( model ) ).to.equal(
' foo []' );
editor.execute( 'undo' );
expect( getModelData( model ) ).to.equal( ' foo[]' );
} );
it( 'should work when indentation is with tabs', () => {
setModelData( model, 'foo[]' );
model.change( writer => {
// foo[]
writer.insertText( ' ', model.document.getRoot().getChild( 0 ), 0 );
} );
viewDoc.fire( 'enter', getEvent() );
expect( getModelData( model ) ).to.equal(
' foo []' );
editor.execute( 'undo' );
expect( getModelData( model ) ).to.equal( ' foo[]' );
} );
it( 'should retain only the last line', () => {
setModelData( model, 'foobar[]' );
model.change( writer => {
// foo bar[]
writer.insertText( ' ', model.document.getRoot().getChild( 0 ), 4 );
writer.insertText( ' ', model.document.getRoot().getChild( 0 ), 0 );
} );
viewDoc.fire( 'enter', getEvent() );
expect( getModelData( model ) ).to.equal(
' foo bar []' );
editor.execute( 'undo' );
expect( getModelData( model ) ).to.equal( ' foo bar[]' );
} );
it( 'should retain when the selection is non–collapsed', () => {
setModelData( model, 'f[o]o' );
model.change( writer => {
// f[o]o
writer.insertText( ' ', model.document.getRoot().getChild( 0 ), 0 );
} );
viewDoc.fire( 'enter', getEvent() );
expect( getModelData( model ) ).to.equal(
' f []o' );
editor.execute( 'undo' );
expect( getModelData( model ) ).to.equal( ' f[o]o' );
} );
it( 'should consider only leading white-spaces', () => {
setModelData( model, 'foo[]' );
model.change( writer => {
// foo []
writer.insertText( ' ', model.document.getRoot().getChild( 0 ), 3 );
writer.insertText( ' ', model.document.getRoot().getChild( 0 ), 0 );
} );
viewDoc.fire( 'enter', getEvent() );
expect( getModelData( model ) ).to.equal(
' foo []' );
editor.execute( 'undo' );
expect( getModelData( model ) ).to.equal( ' foo []' );
} );
it( 'should not work when there is some non-whitespace character', () => {
setModelData( model, 'foo[]' );
model.change( writer => {
// foo []
writer.insertText( ' ', model.document.getRoot().getChild( 0 ), 3 );
} );
viewDoc.fire( 'enter', getEvent() );
expect( getModelData( model ) ).to.equal(
'foo []' );
} );
} );
describe( 'leaving block on double enter', () => {
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', getEvent() );
expect( getModelData( model ) ).to.equal( 'foo[]' );
viewDoc.fire( 'enter', getEvent() );
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 the selection is not collapsed', () => {
setModelData( model, 'f[oo]' );
viewDoc.fire( 'enter', getEvent() );
expect( getModelData( model ) ).to.equal(
'f[]' );
} );
it( 'should not leave the block when pressed twice when in the middle of the code', () => {
setModelData( model, 'fo[]o' );
viewDoc.fire( 'enter', getEvent() );
expect( getModelData( model ) ).to.equal(
'fo[]o' );
viewDoc.fire( 'enter', getEvent() );
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', getEvent() );
expect( getModelData( model ) ).to.equal(
'[]foo' );
viewDoc.fire( 'enter', getEvent() );
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', getEvent( { isSoft: true } ) );
expect( getModelData( model ) ).to.equal(
'foo[]' );
} );
it( 'should clean up the last line if has white–space characters only', () => {
setModelData( model, 'foo[]' );
model.change( writer => {
// foo []
writer.insertText( ' ', model.document.getRoot().getChild( 0 ), 4 );
} );
viewDoc.fire( 'enter', getEvent() );
expect( getModelData( model ) ).to.equal(
'foo[]' );
} );
} );
function getEvent( data = {} ) {
return new DomEventData( viewDoc, {
preventDefault: sinon.spy()
}, data );
}
} );
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( '', 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
\nBar
' );
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( '[]' );
} );
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 );
} );
describe( 'getSelectedContent()', () => {
it( 'should not engage when there is nothing selected', () => {
setModelData( model, 'fo[]obar' );
expect( stringify( model.getSelectedContent( model.document.selection ) ) ).to.equal( '' );
} );
it( 'should wrap a partial multi-line selection into a code block (#1)', () => {
setModelData( model, 'fo[ob]ar' );
expect( stringify( model.getSelectedContent( model.document.selection ) ) ).to.equal(
'ob'
);
} );
it( 'should wrap a partial multi-line selection into a code block (#2)', () => {
setModelData( model, 'fo[o]bar' );
expect( stringify( model.getSelectedContent( model.document.selection ) ) ).to.equal(
'o'
);
} );
it( 'should wrap a partial multi-line selection into a code block (#3)', () => {
setModelData( model, '[foobar]' );
expect( stringify( model.getSelectedContent( model.document.selection ) ) ).to.equal(
'foobar'
);
} );
it( 'should wrap a complete single-line selection into a code block', () => {
setModelData( model, '[foo]' );
expect( stringify( model.getSelectedContent( model.document.selection ) ) ).to.equal(
'foo'
);
} );
it( 'should wrap a partial single-line selection into an inline code (#1)', () => {
setModelData( model, '[fo]obar' );
expect( stringify( model.getSelectedContent( model.document.selection ) ) ).to.equal(
'<$text code="true">fo$text>'
);
} );
it( 'should wrap a partial single-line selection into an inline code (#2)', () => {
setModelData( model, 'foob[a]r' );
expect( stringify( model.getSelectedContent( model.document.selection ) ) ).to.equal(
'<$text code="true">a$text>'
);
} );
it( 'should preserve a code block in a cross-selection (#1)', () => {
setModelData( model, '[xfo]obar' );
expect( stringify( model.getSelectedContent( model.document.selection ) ) ).to.equal(
'xfo'
);
} );
it( 'should preserve a code block in a cross-selection (#2)', () => {
setModelData( model, '[xfoob]ar' );
expect( stringify( model.getSelectedContent( model.document.selection ) ) ).to.equal(
'xfoob'
);
} );
it( 'should preserve a code block in a cross-selection (#3)', () => {
setModelData( model, 'foob[arx]' );
expect( stringify( model.getSelectedContent( model.document.selection ) ) ).to.equal(
'arx'
);
} );
} );
} );
} );