/**
* @license Copyright (c) 2003-2020, 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 IndentCodeBlockCommand from '../src/indentcodeblockcommand';
import OutdentCodeBlockCommand from '../src/outdentcodeblockcommand';
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 IndentEditing from '@ckeditor/ckeditor5-indent/src/indentediting';
import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
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( [
{ language: 'plaintext', label: 'Plain text' },
{ language: 'c', label: 'C' },
{ language: 'cs', label: 'C#' },
{ language: 'cpp', label: 'C++' },
{ language: 'css', label: 'CSS' },
{ language: 'diff', label: 'Diff' },
{ language: 'html', label: 'HTML' },
{ language: 'java', label: 'Java' },
{ language: 'javascript', label: 'JavaScript' },
{ language: 'php', label: 'PHP' },
{ language: 'python', label: 'Python' },
{ language: 'ruby', label: 'Ruby' },
{ language: 'typescript', label: 'TypeScript' },
{ language: 'xml', label: 'XML' }
] );
} );
} );
} );
describe( 'indentSequence', () => {
describe( 'default value', () => {
it( 'should be set', () => {
expect( editor.config.get( 'codeBlock.indentSequence' ) ).to.equal( ' ' );
} );
} );
} );
} );
it( 'adds a "codeBlock" command', () => {
expect( editor.commands.get( 'codeBlock' ) ).to.be.instanceOf( CodeBlockCommand );
} );
it( 'adds an "indentCodeBlock" command', () => {
expect( editor.commands.get( 'indentCodeBlock' ) ).to.be.instanceOf( IndentCodeBlockCommand );
} );
it( 'adds an "outdentCodeBlock" command', () => {
expect( editor.commands.get( 'outdentCodeBlock' ) ).to.be.instanceOf( OutdentCodeBlockCommand );
} );
it( 'allows for codeBlock in the $root', () => {
expect( model.schema.checkChild( [ '$root' ], 'codeBlock' ) ).to.be.true;
} );
it( 'disallows for codeBlock in the other codeBlock', () => {
expect( model.schema.checkChild( [ '$root', 'codeBlock' ], 'codeBlock' ) ).to.be.false;
} );
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, '
\t\tx\n\tx' );
editor.model.change( writer => {
writer.setSelection( editor.model.document.getRoot().getChild( 0 ), 'on' );
} );
editor.execute( 'indent' );
expect( getModelData( editor.model ) ).to.equal(
'' +
'[]' +
'' );
} );
it( 'should convert non-empty codeBlock to pre tag', () => {
setModelData( model, '' +
'{}Foo' +
'' );
} );
it( 'should convert codeBlock with softBreaks to pre tag #1', () => {
setModelData( model,
'' +
'{}Foo
Bar
Biz' +
'' );
} );
it( 'should convert codeBlock with softBreaks to pre tag #2', () => {
setModelData( model,
'' +
'[]
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' +
'' );
element.remove();
return editor.destroy();
} );
} );
it( 'should not set the class on the if it was configured so', () => {
const element = document.createElement( 'div' );
document.body.appendChild( element );
return ClassicTestEditor
.create( element, {
plugins: [ CodeBlockEditing, AlignmentEditing, BoldEditing, Enter, Paragraph ],
codeBlock: {
languages: [
{ language: 'cpp', label: 'C++', class: '' }
]
}
} )
.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' +
' ' +
'A B '
);
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, 'Foo Bar ' );
expect( editor.getData() ).to.equal( 'Foo\nBar' );
} );
it( 'should not set the class on the if it was configured so', () => {
const element = document.createElement( 'div' );
document.body.appendChild( element );
return ClassicTestEditor
.create( element, {
plugins: [ CodeBlockEditing, AlignmentEditing, BoldEditing, Enter, Paragraph ],
codeBlock: {
languages: [
{ language: 'cpp', label: 'C++', class: '' }
]
}
} )
.then( newEditor => {
const editor = newEditor;
const model = editor.model;
setModelData( model, 'foo ' );
expect( editor.getData() ).to.equal( 'foo
' );
element.remove();
return editor.destroy();
} );
} );
it( 'should set multiple classes on the if it was configured so', () => {
const element = document.createElement( 'div' );
document.body.appendChild( element );
return ClassicTestEditor
.create( element, {
plugins: [ CodeBlockEditing, AlignmentEditing, BoldEditing, Enter, Paragraph ],
codeBlock: {
languages: [
{ language: 'javascript', label: 'JavaScript', class: 'language-js' },
{ language: 'swift', label: 'Swift', class: 'swift ios-code' }
]
}
} )
.then( editor => {
const model = editor.model;
setModelData( model,
'foo ' +
'foo '
);
expect( editor.getData() ).to.equal(
'foo
' +
'foo
'
);
element.remove();
return editor.destroy();
} );
} );
} );
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' +
' ' +
' ' +
' '
);
} );
// Undesired by expected. There is an issue with identifying the correct filler type.
// is inline, so dom-to-view converter expects an inline filler.
it( 'should convert pre > code with only inside to a codeBlock with ', () => {
editor.setData( '
' );
expect( getModelData( model ) ).to.equal(
'[]\u00a0 '
);
} );
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 and use only the text content of invalid HTML tags', () => {
editor.setData( 'Foo
Bar
Biz
' );
expect( getModelData( model ) ).to.equal(
'[]FooBarBiz ' );
} );
it( 'should convert pre > code tag with escaped html content', () => {
editor.setData( '<div><p>Foo's&"bar"</p></div>
' );
expect( getModelData( model ) ).to.equal( '[]Foo\'s&"bar"
' );
} );
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(
'[]foo bar ' );
} );
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( 'config.codeBlock.languages', () => {
it( 'should be respected when upcasting', () => {
return ClassicTestEditor.create(
'bar
' +
'baz
' +
'qux
',
{
plugins: [ CodeBlockEditing ],
codeBlock: {
languages: [
{ language: 'foo', label: 'Foo' },
{ language: 'bar', label: 'Bar' },
{ language: 'qux', label: 'Qux', class: 'qux' }
]
}
} )
.then( editor => {
model = editor.model;
expect( getModelData( model ) ).to.equal(
'[]bar ' +
'baz ' +
'qux '
);
return editor.destroy();
} );
} );
it( 'should be respected when upcasting and a language has an empty class configured', () => {
return ClassicTestEditor.create(
'bar
' +
'baz
' +
'qux
',
{
plugins: [ CodeBlockEditing ],
codeBlock: {
languages: [
{ language: 'foo', label: 'Foo' },
{ language: 'bar', label: 'Bar' },
{ language: 'qux', label: 'Qux', class: '' }
]
}
} )
.then( editor => {
model = editor.model;
expect( getModelData( model ) ).to.equal(
'[]bar ' +
'baz ' +
'qux '
);
return editor.destroy();
} );
} );
it( 'should upcast using the first language if the code in data has no language', () => {
return ClassicTestEditor
.create( 'bar
', {
plugins: [ CodeBlockEditing ],
codeBlock: {
languages: [
{ language: 'foo', label: 'Foo' },
{ language: 'bar', label: 'Bar' }
]
}
} )
.then( editor => {
model = editor.model;
expect( getModelData( model ) ).to.equal( '[]bar ' );
return editor.destroy();
} );
} );
it( 'should upast using the first language if the code in data has an invalid language', () => {
return ClassicTestEditor
.create( 'bar
', {
plugins: [ CodeBlockEditing ],
codeBlock: {
languages: [
{ language: 'foo', label: 'Foo' },
{ language: 'bar', label: 'Bar' }
]
}
} )
.then( editor => {
model = editor.model;
expect( getModelData( model ) ).to.equal( '[]bar ' );
return editor.destroy();
} );
} );
// https://github.com/ckeditor/ckeditor5/issues/5924
it( 'should upcast using only the first class from config as a defining language class', () => {
// "baz" is the first class configured for the "baz" language.
return ClassicTestEditor.create( 'foo
', {
plugins: [ CodeBlockEditing ],
codeBlock: {
languages: [
{ language: 'foo', label: 'Foo', class: 'foo' },
{ language: 'baz', label: 'Baz', class: 'baz bar' },
{ language: 'qux', label: 'Qux', class: 'qux' }
]
}
} ).then( editor => {
model = editor.model;
expect( getModelData( model ) ).to.equal( '[]foo ' );
return editor.destroy();
} );
} );
// https://github.com/ckeditor/ckeditor5/issues/5924
it( 'should not upcast if the class is not the first (defining) language class', () => {
// "bar" is the second class configured for the "baz" language.
return ClassicTestEditor.create( 'foo
', {
plugins: [ CodeBlockEditing ],
codeBlock: {
languages: [
{ language: 'foo', label: 'Foo', class: 'foo' },
{ language: 'baz', label: 'Baz', class: 'baz bar' },
{ language: 'qux', label: 'Qux', class: 'qux' }
]
}
} ).then( editor => {
model = editor.model;
expect( getModelData( model ) ).to.equal( '[]foo ' );
return editor.destroy();
} );
} );
} );
} );
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[]o bar ' );
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[o b]ar ' );
expect( stringify( model.getSelectedContent( model.document.selection ) ) ).to.equal(
'o b '
);
} );
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, '[foo bar] ' );
expect( stringify( model.getSelectedContent( model.document.selection ) ) ).to.equal(
'foo bar '
);
} );
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)', () => {
model.schema.extend( '$text', {
allowAttributes: 'code'
} );
setModelData( model, '[fo]o bar ' );
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)', () => {
model.schema.extend( '$text', {
allowAttributes: 'code'
} );
setModelData( model, 'foo b[a]r ' );
expect( stringify( model.getSelectedContent( model.document.selection ) ) ).to.equal(
'<$text code="true">a$text>'
);
} );
it( 'should now wrap a partial single-line selection into an inline code when the attribute is disallowed', () => {
setModelData( model, 'foo b[a]r ' );
expect( stringify( model.getSelectedContent( model.document.selection ) ) ).to.equal( 'a' );
} );
it( 'should preserve a code block in a cross-selection (#1)', () => {
setModelData( model,
'[x fo]o bar ' );
expect( stringify( model.getSelectedContent( model.document.selection ) ) ).to.equal(
'x fo '
);
} );
it( 'should preserve a code block in a cross-selection (#2)', () => {
setModelData( model,
'[x foo b]ar ' );
expect( stringify( model.getSelectedContent( model.document.selection ) ) ).to.equal(
'x foo b '
);
} );
it( 'should preserve a code block in a cross-selection (#3)', () => {
setModelData( model,
'foo b[ar x] ' );
expect( stringify( model.getSelectedContent( model.document.selection ) ) ).to.equal(
'ar x '
);
} );
} );
} );
} );