/** * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md. */ import Conversion from '../../src/conversion/conversion'; import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import UpcastDispatcher from '../../src/conversion/upcastdispatcher'; import { helpers as upcastHelpers, convertText, convertToModelFragment } from '../../src/conversion/upcast-converters'; import EditingController from '../../src/controller/editingcontroller'; import Model from '../../src/model/model'; import { parse as viewParse, stringify as viewStringify } from '../../src/dev-utils/view'; import { setData, stringify as modelStringify } from '../../src/dev-utils/model'; import { helpers as downcastHelpers } from '../../src/conversion/downcast-converters'; describe( 'Conversion', () => { let conversion, dispA, dispB; beforeEach( () => { conversion = new Conversion(); // Placeholders. Will be used only to see if their were given as attribute for a spy function. dispA = Symbol( 'dispA' ); dispB = Symbol( 'dispB' ); conversion.register( { name: 'ab', dispatcher: [ dispA, dispB ] } ); conversion.register( { name: 'a', dispatcher: dispA } ); conversion.register( { name: 'b', dispatcher: dispB } ); } ); describe( 'register()', () => { it( 'should throw when trying to use same group name twice', () => { expect( () => { conversion.register( { name: 'ab' } ); } ).to.throw( CKEditorError, /conversion-register-group-exists/ ); } ); } ); describe( 'for()', () => { it( 'should return object with .add() method', () => { const forResult = conversion.for( 'ab' ); expect( forResult.add ).to.be.instanceof( Function ); } ); it( 'should throw if non-existing group name has been used', () => { expect( () => { conversion.for( 'foo' ); } ).to.throw( CKEditorError, /conversion-for-unknown-group/ ); } ); } ); describe( 'add()', () => { let helperA, helperB; beforeEach( () => { helperA = sinon.stub(); helperB = sinon.stub(); } ); it( 'should be chainable', () => { const forResult = conversion.for( 'ab' ); const addResult = forResult.add( () => {} ); expect( addResult ).to.equal( addResult.add( () => {} ) ); } ); it( 'should fire given helper for every dispatcher in given group', () => { conversion.for( 'ab' ).add( helperA ); expect( helperA.calledWithExactly( dispA ) ).to.be.true; expect( helperA.calledWithExactly( dispB ) ).to.be.true; conversion.for( 'b' ).add( helperB ); expect( helperB.calledWithExactly( dispA ) ).to.be.false; expect( helperB.calledWithExactly( dispB ) ).to.be.true; } ); } ); describe( 'converters', () => { let viewDispatcher, model, schema, conversion, modelRoot, viewRoot; beforeEach( () => { model = new Model(); const controller = new EditingController( model ); const modelDoc = model.document; modelRoot = modelDoc.createRoot(); viewRoot = controller.view.document.getRoot(); // Set name of view root the same as dom root. // This is a mock of attaching view root to dom root. viewRoot._name = 'div'; schema = model.schema; schema.extend( '$text', { allowAttributes: [ 'bold' ] } ); schema.register( 'paragraph', { inheritAllFrom: '$block' } ); viewDispatcher = new UpcastDispatcher( model, { schema } ); viewDispatcher.on( 'text', convertText() ); viewDispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } ); viewDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } ); conversion = new Conversion(); conversion.register( { name: 'upcast', dispatcher: [ viewDispatcher ], helpers: upcastHelpers } ); conversion.register( { name: 'downcast', dispatcher: [ controller.downcastDispatcher ], helpers: downcastHelpers } ); } ); describe( 'elementToElement', () => { it( 'config.view is a string', () => { conversion.elementToElement( { model: 'paragraph', view: 'p' } ); test( '
Foo
', 'Foo
', 'Foo
', 'Foo
', 'Foo
' ); test( 'Foo', 'Foo
' ); } ); it( 'upcastAlso given as a function', () => { schema.register( 'heading', { inheritAllFrom: '$block' } ); conversion.elementToElement( { model: 'heading', view: 'h2', upcastAlso: viewElement => { const fontSize = viewElement.getStyle( 'font-size' ); if ( !fontSize ) { return null; } const match = fontSize.match( /(\d+)\s*px/ ); if ( !match ) { return null; } const size = Number( match[ 1 ] ); if ( size >= 26 ) { return { name: true, style: [ 'font-size' ] }; } return null; } } ); conversion.elementToElement( { model: 'paragraph', view: 'p' } ); test( '', 'Foo bar
', 'Foo
', 'Foo
', 'Foo
' ); } ); it( 'config.view is an object', () => { conversion.attributeToElement( { model: 'bold', view: { name: 'span', classes: 'bold' } } ); test( 'Foo bar
', 'Foo
', 'Foo
', 'Foo
' ); test( 'Foo
', 'Foo
' ); test( 'Foo
', 'Foo
' ); test( 'Foo
', 'Foo
' ); test( 'Foo
', 'Foo
' ); } ); it( 'model attribute value is enumerable', () => { schema.extend( '$text', { allowAttributes: [ 'fontSize' ] } ); conversion.attributeToElement( { model: { key: 'fontSize', values: [ 'big', 'small' ] }, view: { big: { name: 'span', styles: { 'font-size': '1.2em' } }, small: { name: 'span', styles: { 'font-size': '0.8em' } } }, upcastAlso: { big: viewElement => { const fontSize = viewElement.getStyle( 'font-size' ); if ( !fontSize ) { return null; } const match = fontSize.match( /(\d+)\s*px/ ); if ( !match ) { return null; } const size = Number( match[ 1 ] ); if ( viewElement.is( 'span' ) && size > 10 ) { return { name: true, style: [ 'font-size' ] }; } return null; }, small: viewElement => { const fontSize = viewElement.getStyle( 'font-size' ); if ( !fontSize ) { return null; } const match = fontSize.match( /(\d+)\s*px/ ); if ( !match ) { return null; } const size = Number( match[ 1 ] ); if ( viewElement.is( 'span' ) && size < 10 ) { return { name: true, style: [ 'font-size' ] }; } return null; } } } ); test( 'Foo bar
', 'Foo bar
', 'Foo bar
' ); test( 'Foo bar
', 'Foo bar
', 'Foo bar
' ); test( 'Foo bar
', 'Foo bar
' ); } ); it( 'config.model.name is given', () => { schema.extend( '$text', { allowAttributes: [ 'textDecoration' ] } ); conversion.attributeToElement( { model: { key: 'textDecoration', values: [ 'underline', 'lineThrough' ], name: '$text' }, view: { underline: { name: 'span', styles: { 'text-decoration': 'underline' } }, lineThrough: { name: 'span', styles: { 'text-decoration': 'line-through' } } } } ); test( 'Foo
', 'Foo
', 'Foo
', '
', 'Foo
', 'Foo
', 'Foo
' ); test( 'Foo
', 'Foo
', 'Foo
' ); } ); it( 'config.view and config.model have name and key set', () => { schema.extend( 'image', { allowAttributes: [ 'source' ] } ); conversion.attributeToAttribute( { model: { name: 'image', key: 'source' }, view: { name: 'img', key: 'src' } } ); test( '
', 'foo
' ); } ); } ); describe( 'attributeToElement()', () => { it( 'adds downcast converter', () => { conversion.for( 'downcast' ).elementToElement( { model: 'paragraph', view: 'p' } ); conversion.for( 'downcast' ).attributeToElement( { model: 'bold', view: 'strong' } ); testDowncast( 'Foo bar
' ); } ); } ); describe( 'attributeToAttribute()', () => { it( 'adds downcast converter', () => { schema.register( 'image', { inheritAllFrom: '$block', allowAttributes: [ 'source' ] } ); conversion.for( 'downcast' ).elementToElement( { model: 'image', view: 'img' } ); conversion.for( 'downcast' ).attributeToAttribute( { model: 'source', view: 'src' } ); testDowncast( '
' );
} );
} );
describe( 'markerToElement()', () => {
it( 'adds downcast converter', () => {
conversion.for( 'downcast' ).markerToElement( { model: 'search', view: 'marker-search' } );
model.change( writer => {
writer.insertText( 'foo', modelRoot, 0 );
const range = writer.createRange(
writer.createPositionAt( modelRoot, 1 ),
writer.createPositionAt( modelRoot, 2 )
);
writer.addMarker( 'search', { range, usingOperation: false } );
} );
expect( viewStringify( viewRoot, null, { ignoreRoot: true } ) )
.to.equal( 'ffoo
', 'Foo bar
', '