Skip to content

Commit

Permalink
Merge pull request #11075 from ckeditor/ck/10891-script-in-ghs
Browse files Browse the repository at this point in the history
Feature (html-support): Adds support for `<script>` elements. Closes #10891.
  • Loading branch information
niegowski authored Jan 11, 2022
2 parents 807b60f + ed38d6e commit 277a591
Show file tree
Hide file tree
Showing 5 changed files with 266 additions and 1 deletion.
2 changes: 2 additions & 0 deletions packages/ckeditor5-html-support/src/generalhtmlsupport.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import DualContentModelElementSupport from './integrations/dualcontent';
import HeadingElementSupport from './integrations/heading';
import ImageElementSupport from './integrations/image';
import MediaEmbedElementSupport from './integrations/mediaembed';
import ScriptElementSupport from './integrations/script';
import TableElementSupport from './integrations/table';

/**
Expand Down Expand Up @@ -44,6 +45,7 @@ export default class GeneralHtmlSupport extends Plugin {
HeadingElementSupport,
ImageElementSupport,
MediaEmbedElementSupport,
ScriptElementSupport,
TableElementSupport
];
}
Expand Down
73 changes: 73 additions & 0 deletions packages/ckeditor5-html-support/src/integrations/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/**
* @module html-support/integrations/script
*/

import { Plugin } from 'ckeditor5/src/core';
import {
createObjectView,
modelToViewBlockAttributeConverter,
viewToModelBlockAttributeConverter,
viewToModelObjectConverter
} from '../converters.js';

import DataFilter from '../datafilter';

/**
* Provides the General HTML Support for `script` elements.
*
* @extends module:core/plugin~Plugin
*/
export default class ScriptElementSupport extends Plugin {
/**
* @inheritDoc
*/
static get requires() {
return [ DataFilter ];
}

/**
* @inheritDoc
*/
init() {
const dataFilter = this.editor.plugins.get( DataFilter );

dataFilter.on( 'register:script', ( evt, definition ) => {
const editor = this.editor;
const schema = editor.model.schema;
const conversion = editor.conversion;

schema.register( 'htmlScript', definition.modelSchema );

schema.extend( 'htmlScript', {
allowAttributes: [ 'htmlAttributes', 'htmlContent' ]
} );

editor.data.registerRawContentMatcher( {
name: 'script'
} );

conversion.for( 'upcast' ).elementToElement( {
view: 'script',
model: viewToModelObjectConverter( definition )
} );

conversion.for( 'upcast' ).add( viewToModelBlockAttributeConverter( definition, dataFilter ) );

conversion.for( 'downcast' ).elementToElement( {
model: 'htmlScript',
view: ( modelElement, { writer } ) => {
return createObjectView( 'script', modelElement, writer );
}
} );

conversion.for( 'downcast' ).add( modelToViewBlockAttributeConverter( definition ) );

evt.stop();
} );
}
}
9 changes: 8 additions & 1 deletion packages/ckeditor5-html-support/src/schemadefinitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
//
// Skipped hidden elements:
// noscript
// script

export default {
block: [
Expand Down Expand Up @@ -832,6 +831,14 @@ export default {
modelSchema: {
inheritAllFrom: '$htmlObjectInline'
}
},
{
model: 'htmlScript',
view: 'script',
modelSchema: {
allowWhere: [ '$text', '$block' ],
isInline: true
}
}
]
};
179 changes: 179 additions & 0 deletions packages/ckeditor5-html-support/tests/integrations/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/**
* @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import GeneralHtmlSupport from '../../src/generalhtmlsupport';
import { getModelDataWithAttributes } from '../_utils/utils';
import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';

/* global console, document */

describe( 'ScriptElementSupport', () => {
const CODE = 'console.log( "Hello World" )';
const CODE_CPP = 'cout << "Hello World" << endl;';

let editor, model, editorElement, dataFilter, warnStub;

beforeEach( async () => {
editorElement = document.createElement( 'div' );
document.body.appendChild( editorElement );

editor = await ClassicTestEditor.create( editorElement, {
plugins: [ Paragraph, GeneralHtmlSupport ]
} );
model = editor.model;
dataFilter = editor.plugins.get( 'DataFilter' );

dataFilter.allowElement( 'script' );

warnStub = sinon.stub( console, 'warn' );
} );

afterEach( () => {
warnStub.restore();
editorElement.remove();

return editor.destroy();
} );

it( 'should allow element', () => {
editor.setData( `<p>Foo</p><script>${ CODE }</script>` );

expect( getModelData( model, { withoutSelection: true } ) ).to.equal(
`<paragraph>Foo</paragraph><htmlScript htmlContent="${ CODE }"></htmlScript>`
);

expect( editor.getData() ).to.equal( `<p>Foo</p><script>${ CODE }</script>` );
} );

it( 'should allow attributes', () => {
dataFilter.allowAttributes( { name: 'script', attributes: [ 'type', 'nonce' ] } );

editor.setData( `<p>Foo</p><script type="c++" nonce="qwerty">${ CODE_CPP }</script>` );

expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( {
data: `<paragraph>Foo</paragraph><htmlScript htmlAttributes="(1)" htmlContent="${ CODE_CPP }"></htmlScript>`,
attributes: {
1: {
attributes: {
nonce: 'qwerty',
type: 'c++'
}
},
2: CODE_CPP
}
} );

expect( editor.getData() ).to.equal( `<p>Foo</p><script type="c++" nonce="qwerty">${ CODE_CPP }</script>` );
} );

it( 'should disallow attributes', () => {
dataFilter.allowAttributes( { name: 'script', attributes: true } );
dataFilter.disallowAttributes( { name: 'script', attributes: 'nonce' } );

editor.setData( `<p>Foo</p><script type="c++" nonce="qwerty">${ CODE_CPP }</script>` );

expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( {
data: `<paragraph>Foo</paragraph><htmlScript htmlAttributes="(1)" htmlContent="${ CODE_CPP }"></htmlScript>`,
attributes: {
1: {
attributes: {
type: 'c++'
}
},
2: CODE_CPP
}
} );

expect( editor.getData() ).to.equal( `<p>Foo</p><script type="c++">${ CODE_CPP }</script>` );
} );

describe( 'element position', () => {
const testCases = [ {
name: 'paragraph',
data: `<article><section><p>Foo<script>${ CODE }</script>Bar</p></section></article>`,
model:
'<htmlArticle>' +
`<htmlSection><paragraph>Foo<htmlScript htmlContent="${ CODE }"></htmlScript>Bar</paragraph></htmlSection>` +
'</htmlArticle>'
}, {
name: 'section',
data: `<article><section><p>Foo</p><script>${ CODE }</script></section></article>`,
model:
'<htmlArticle>' +
`<htmlSection><paragraph>Foo</paragraph><htmlScript htmlContent="${ CODE }"></htmlScript></htmlSection>` +
'</htmlArticle>'
}, {
name: 'article',
data: `<article><section><p>Foo</p></section><script>${ CODE }</script></article>`,
model:
'<htmlArticle>' +
`<htmlSection><paragraph>Foo</paragraph></htmlSection><htmlScript htmlContent="${ CODE }"></htmlScript>` +
'</htmlArticle>'
}, {
name: 'root',
data: `<article><section><p>Foo</p></section></article><script>${ CODE }</script>`,
model:
'<htmlArticle><htmlSection><paragraph>Foo</paragraph></htmlSection></htmlArticle>' +
`<htmlScript htmlContent="${ CODE }"></htmlScript>`
} ];

for ( const { name, data, model: modelData } of testCases ) {
it( `should allow element inside ${ name }`, () => {
dataFilter.allowElement( 'article' );
dataFilter.allowElement( 'section' );

editor.setData( data );

expect( getModelData( model, { withoutSelection: true } ) ).to.equal( modelData );

expect( editor.getData() ).to.equal( data );
} );
}
} );

it( 'should not consume attributes already consumed (downcast)', () => {
dataFilter.allowAttributes( { name: 'script', attributes: true } );

editor.conversion.for( 'downcast' ).add( dispatcher => {
dispatcher.on( 'attribute:htmlAttributes:htmlScript', ( evt, data, conversionApi ) => {
conversionApi.consumable.consume( data.item, evt.name );
}, { priority: 'high' } );
} );

editor.setData( `<p>Foo</p><script nonce="qwerty">${ CODE }</script>` );

expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( {
data: `<paragraph>Foo</paragraph><htmlScript htmlAttributes="(1)" htmlContent="${ CODE }"></htmlScript>`,
attributes: {
1: { attributes: { nonce: 'qwerty' } },
2: CODE
}
} );

expect( editor.getData() ).to.equal( `<p>Foo</p><script>${ CODE }</script>` );
} );

it( 'should not consume attributes already consumed (upcast)', () => {
dataFilter.allowAttributes( { name: 'script', attributes: true } );

editor.conversion.for( 'upcast' ).add( dispatcher => {
dispatcher.on( 'element:script', ( evt, data, conversionApi ) => {
conversionApi.consumable.consume( data.viewItem, { attributes: 'nonce' } );
}, { priority: 'high' } );
} );

editor.setData( `<p>Foo</p><script type="c++" nonce="qwerty">${ CODE_CPP }</script>` );

expect( getModelDataWithAttributes( model, { withoutSelection: true } ) ).to.deep.equal( {
data: `<paragraph>Foo</paragraph><htmlScript htmlAttributes="(1)" htmlContent="${ CODE_CPP }"></htmlScript>`,
attributes: {
1: { attributes: { type: 'c++' } },
2: CODE_CPP
}
} );
} );
} );
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ ClassicEditor
}
],
styles: { 'background-color': true }
},
{
name: 'script',
attributes: true
}
],
disallow: [
Expand Down

0 comments on commit 277a591

Please sign in to comment.