Skip to content

Commit

Permalink
Merge pull request #15349 from ckeditor/ck/14933
Browse files Browse the repository at this point in the history
Fix (html-support): The editor should not crash when there is a `<template>` element in the content. Closes #14933.
  • Loading branch information
arkflpc authored Nov 20, 2023
2 parents 1c3b5a3 + ff04bf3 commit 3388cfc
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 39 deletions.
39 changes: 25 additions & 14 deletions packages/ckeditor5-html-support/src/integrations/customelement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ export default class CustomElementSupport extends Plugin {
isContent: true
} );

// For the `<template>` element we use only raw-content because DOM API exposes its content
// only as a document fragment in the `content` property (or innerHTML).
editor.data.htmlProcessor.domConverter.registerRawContentMatcher( { name: 'template' } );

// Being executed on the low priority, it will catch all elements that were not caught by other converters.
conversion.for( 'upcast' ).elementToElement( {
view: /.*/,
Expand Down Expand Up @@ -95,10 +99,27 @@ export default class CustomElementSupport extends Plugin {
conversionApi.writer.setAttribute( 'htmlCustomElementAttributes', htmlAttributes, modelElement );
}

// Store the whole element in the attribute so that DomConverter will be able to use the pre like element context.
const viewWriter = new UpcastWriter( viewElement.document );
const documentFragment = viewWriter.createDocumentFragment( viewElement );
const htmlContent = editor.data.processor.toData( documentFragment );
let htmlContent;

// For the `<template>` element we use only raw-content because DOM API exposes its content
// only as a document fragment in the `content` property.
if ( viewElement.is( 'element', 'template' ) && viewElement.getCustomProperty( '$rawContent' ) ) {
htmlContent = viewElement.getCustomProperty( '$rawContent' );
} else {
// Store the whole element in the attribute so that DomConverter will be able to use the pre like element context.
const viewWriter = new UpcastWriter( viewElement.document );
const documentFragment = viewWriter.createDocumentFragment( viewElement );
const domFragment = editor.data.htmlProcessor.domConverter.viewToDom( documentFragment );
const domElement = domFragment.firstChild!;

while ( domElement.firstChild ) {
domFragment.appendChild( domElement.firstChild );
}

domElement.remove();

htmlContent = editor.data.htmlProcessor.htmlWriter.getHtml( domFragment );
}

conversionApi.writer.setAttribute( 'htmlContent', htmlContent, modelElement );

Expand Down Expand Up @@ -146,16 +167,6 @@ export default class CustomElementSupport extends Plugin {

const viewElement = writer.createRawElement( viewName, null, ( domElement, domConverter ) => {
domConverter.setContentOf( domElement, htmlContent );

// Unwrap the custom element content (it was stored in the attribute as the whole custom element).
// See the upcast conversion for the "htmlContent" attribute to learn more.
const customElement = domElement.firstChild!;

customElement.remove();

while ( customElement.firstChild ) {
domElement.appendChild( customElement.firstChild );
}
} );

if ( modelElement.hasAttribute( 'htmlCustomElementAttributes' ) ) {
Expand Down
97 changes: 73 additions & 24 deletions packages/ckeditor5-html-support/tests/integrations/customelement.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,29 @@ describe( 'CustomElementSupport', () => {

expect( getModelDataWithAttributes( model, { withoutSelection: true, excludeAttributes } ) ).to.deep.equal( {
data: '<htmlCustomElement' +
' htmlContent="<custom-foo-element>bar</custom-foo-element>"' +
' htmlContent="bar"' +
' htmlElementName="custom-foo-element"></htmlCustomElement>',
attributes: {}
} );

expect( editor.getData() ).to.equal( '<custom-foo-element>bar</custom-foo-element>' );
} );

// See https://github.com/ckeditor/ckeditor5/issues/14933.
it( 'should allow <template> element', () => {
dataFilter.allowElement( /.*/ );
editor.setData( '<template>bar</template>' );

expect( getModelDataWithAttributes( model, { withoutSelection: true, excludeAttributes } ) ).to.deep.equal( {
data: '<htmlCustomElement' +
' htmlContent="bar"' +
' htmlElementName="template"></htmlCustomElement>',
attributes: {}
} );

expect( editor.getData() ).to.equal( '<template>bar</template>' );
} );

it( 'should not allow unknown custom element if allow-all is not enabled', () => {
// Note that this one does not match any element in the DataSchema. As a result, no upcast conversion will occur.
dataFilter.allowElement( /custom-foo-element/ );
Expand Down Expand Up @@ -96,10 +111,10 @@ describe( 'CustomElementSupport', () => {

expect( getModelDataWithAttributes( model, { withoutSelection: true, excludeAttributes } ) ).to.deep.equal( {
data: '<htmlCustomElement' +
' htmlContent="<custom-foo-element>bar</custom-foo-element>"' +
' htmlContent="bar"' +
' htmlElementName="custom-foo-element"></htmlCustomElement>' +
'<htmlCustomElement' +
' htmlContent="<custom-foo-element>baz</custom-foo-element>"' +
' htmlContent="baz"' +
' htmlElementName="custom-foo-element"></htmlCustomElement>',
attributes: {}
} );
Expand Down Expand Up @@ -134,10 +149,10 @@ describe( 'CustomElementSupport', () => {
expect( getModelDataWithAttributes( model, { withoutSelection: true, excludeAttributes } ) ).to.deep.equal( {
data:
'<htmlCustomElement' +
' htmlContent="<custom-foo-element><nested>a </nested></custom-foo-element>"' +
' htmlContent="<nested>a </nested>"' +
' htmlElementName="custom-foo-element"></htmlCustomElement>' +
'<htmlCustomElement' +
' htmlContent="<custom-foo-element><nested>b</nested></custom-foo-element>"' +
' htmlContent="<nested>b</nested>"' +
' htmlElementName="custom-foo-element"></htmlCustomElement>',
attributes: {}
} );
Expand All @@ -161,7 +176,7 @@ describe( 'CustomElementSupport', () => {
'<paragraph>' +
'Foo' +
'<htmlCustomElement' +
' htmlContent="<custom-foo-element>abc</custom-foo-element>"' +
' htmlContent="abc"' +
' htmlElementName="custom-foo-element">' +
'</htmlCustomElement>' +
'Bar' +
Expand All @@ -176,7 +191,7 @@ describe( 'CustomElementSupport', () => {
'<htmlSection>' +
'<paragraph>Foo</paragraph>' +
'<htmlCustomElement' +
' htmlContent="<custom-foo-element>abc</custom-foo-element>"' +
' htmlContent="abc"' +
' htmlElementName="custom-foo-element">' +
'</htmlCustomElement>' +
'</htmlSection>' +
Expand All @@ -190,7 +205,7 @@ describe( 'CustomElementSupport', () => {
'<paragraph>Foo</paragraph>' +
'</htmlSection>' +
'<htmlCustomElement' +
' htmlContent="<custom-foo-element>abc</custom-foo-element>"' +
' htmlContent="abc"' +
' htmlElementName="custom-foo-element">' +
'</htmlCustomElement>' +
'</htmlArticle>'
Expand All @@ -204,7 +219,7 @@ describe( 'CustomElementSupport', () => {
'</htmlSection>' +
'</htmlArticle>' +
'<htmlCustomElement' +
' htmlContent="<custom-foo-element>abc</custom-foo-element>"' +
' htmlContent="abc"' +
' htmlElementName="custom-foo-element">' +
'</htmlCustomElement>'
} ];
Expand All @@ -231,7 +246,7 @@ describe( 'CustomElementSupport', () => {
data:
'<paragraph>foo ' +
'<htmlCustomElement' +
' htmlContent="<custom>this is <p>some content</p>and more of it </custom>"' +
' htmlContent="this is <p>some content</p>and more of it "' +
' htmlElementName="custom"></htmlCustomElement>' +
'bar</paragraph>',
attributes: {}
Expand All @@ -240,13 +255,31 @@ describe( 'CustomElementSupport', () => {
expect( editor.getData() ).to.equal( '<p>foo <custom>this is <p>some content</p>and more of it </custom>bar</p>' );
} );

// See https://github.com/ckeditor/ckeditor5/issues/14933.
it( 'should preserve <`template> element content', () => {
dataFilter.allowElement( /.*/ );
editor.setData( 'foo <template>this is <p>some content</p> and more of it</template> bar' );

expect( getModelDataWithAttributes( model, { withoutSelection: true, excludeAttributes } ) ).to.deep.equal( {
data:
'<paragraph>foo ' +
'<htmlCustomElement' +
' htmlContent="this is <p>some content</p> and more of it"' +
' htmlElementName="template"></htmlCustomElement> ' +
'bar</paragraph>',
attributes: {}
} );

expect( editor.getData() ).to.equal( '<p>foo&nbsp;<template>this is <p>some content</p> and more of it</template> bar</p>' );
} );

it( 'should not inject nbsp in the element content', () => {
dataFilter.allowElement( /.*/ );
editor.setData( '<custom><custom2> c </custom2></custom>' );

expect( getModelDataWithAttributes( model, { withoutSelection: true, excludeAttributes } ) ).to.deep.equal( {
data: '<htmlCustomElement' +
' htmlContent="<custom><custom2>c</custom2></custom>"' +
' htmlContent="<custom2>c</custom2>"' +
' htmlElementName="custom"></htmlCustomElement>',
attributes: {}
} );
Expand All @@ -264,7 +297,7 @@ describe( 'CustomElementSupport', () => {

expect( getModelDataWithAttributes( model, { withoutSelection: true, excludeAttributes } ) ).to.deep.equal( {
data: '<htmlCustomElement' +
' htmlContent="<custom-foo-element data-foo="foo">bar</custom-foo-element>"' +
' htmlContent="bar"' +
' htmlCustomElementAttributes="(1)"' +
' htmlElementName="custom-foo-element"></htmlCustomElement>',
attributes: {
Expand All @@ -287,7 +320,7 @@ describe( 'CustomElementSupport', () => {

expect( getModelDataWithAttributes( model, { withoutSelection: true, excludeAttributes } ) ).to.deep.equal( {
data: '<htmlCustomElement' +
' htmlContent="<custom-foo-element foo="bar">baz</custom-foo-element>"' +
' htmlContent="baz"' +
' htmlCustomElementAttributes="(1)"' +
' htmlElementName="custom-foo-element"></htmlCustomElement>',
attributes: {
Expand All @@ -312,7 +345,7 @@ describe( 'CustomElementSupport', () => {

expect( getModelDataWithAttributes( model, { withoutSelection: true, excludeAttributes } ) ).to.deep.equal( {
data: '<htmlCustomElement' +
' htmlContent="<custom-foo-element data-foo="bar">baz</custom-foo-element>"' +
' htmlContent="baz"' +
' htmlCustomElementAttributes="(1)"' +
' htmlElementName="custom-foo-element"></htmlCustomElement>',
attributes: {
Expand All @@ -338,7 +371,7 @@ describe( 'CustomElementSupport', () => {

expect( getModelDataWithAttributes( model, { withoutSelection: true, excludeAttributes } ) ).to.deep.equal( {
data: '<htmlCustomElement' +
' htmlContent="<custom-foo-element class="foo">bar</custom-foo-element>"' +
' htmlContent="bar"' +
' htmlCustomElementAttributes="(1)"' +
' htmlElementName="custom-foo-element"></htmlCustomElement>',
attributes: {
Expand All @@ -359,7 +392,7 @@ describe( 'CustomElementSupport', () => {

expect( getModelDataWithAttributes( model, { withoutSelection: true, excludeAttributes } ) ).to.deep.equal( {
data: '<htmlCustomElement' +
' htmlContent="<custom-foo-element style="background:red;">bar</custom-foo-element>"' +
' htmlContent="bar"' +
' htmlCustomElementAttributes="(1)"' +
' htmlElementName="custom-foo-element"></htmlCustomElement>',
attributes: {
Expand All @@ -381,7 +414,7 @@ describe( 'CustomElementSupport', () => {

expect( getModelDataWithAttributes( model, { withoutSelection: true, excludeAttributes } ) ).to.deep.equal( {
data: '<htmlCustomElement' +
' htmlContent="<custom-foo-element>bar</custom-foo-element>"' +
' htmlContent="bar"' +
' htmlElementName="custom-foo-element"' +
' linkHref="bar"' +
'></htmlCustomElement>',
Expand All @@ -400,7 +433,7 @@ describe( 'CustomElementSupport', () => {

expect( getModelDataWithAttributes( model, { withoutSelection: true, excludeAttributes } ) ).to.deep.equal( {
data: '<htmlCustomElement' +
' htmlContent="<custom-foo-element data-foo="foo">bar</custom-foo-element>"' +
' htmlContent="bar"' +
' htmlElementName="custom-foo-element"></htmlCustomElement>',
attributes: {}
} );
Expand All @@ -417,7 +450,7 @@ describe( 'CustomElementSupport', () => {

expect( getModelDataWithAttributes( model, { withoutSelection: true, excludeAttributes } ) ).to.deep.equal( {
data: '<htmlCustomElement' +
' htmlContent="<custom-foo-element class="foo">bar</custom-foo-element>"' +
' htmlContent="bar"' +
' htmlElementName="custom-foo-element"></htmlCustomElement>',
attributes: {}
} );
Expand All @@ -434,7 +467,7 @@ describe( 'CustomElementSupport', () => {

expect( getModelDataWithAttributes( model, { withoutSelection: true, excludeAttributes } ) ).to.deep.equal( {
data: '<htmlCustomElement' +
' htmlContent="<custom-foo-element style="background:red;">bar</custom-foo-element>"' +
' htmlContent="bar"' +
' htmlElementName="custom-foo-element"></htmlCustomElement>',
attributes: {}
} );
Expand All @@ -450,7 +483,7 @@ describe( 'CustomElementSupport', () => {
editor.setData( '<!-- foo --><custom>bar</custom>' );

expect( getModelDataWithAttributes( model, { withoutSelection: true, excludeAttributes } ) ).to.deep.equal( {
data: '<htmlCustomElement htmlContent="<custom>bar</custom>" htmlElementName="custom"></htmlCustomElement>',
data: '<htmlCustomElement htmlContent="bar" htmlElementName="custom"></htmlCustomElement>',
attributes: {}
} );

Expand Down Expand Up @@ -479,15 +512,31 @@ describe( 'CustomElementSupport', () => {
const VALID_ELEMENTS_TEST_DATA = [
[
'<xmlfoo>bar</xmlfoo>',
'<htmlCustomElement htmlContent="<xmlfoo>bar</xmlfoo>" htmlElementName="xmlfoo"></htmlCustomElement>'
'<htmlCustomElement htmlContent="bar" htmlElementName="xmlfoo"></htmlCustomElement>'
],
[
'<foo:bar>baz</foo:bar>',
'<htmlCustomElement htmlContent="<foo:bar>baz</foo:bar>" htmlElementName="foo:bar"></htmlCustomElement>'
'<htmlCustomElement htmlContent="baz" htmlElementName="foo:bar"></htmlCustomElement>'
],
[
'<foo-bar>baz</foo-bar>',
'<htmlCustomElement htmlContent="<foo-bar>baz</foo-bar>" htmlElementName="foo-bar"></htmlCustomElement>'
'<htmlCustomElement htmlContent="baz" htmlElementName="foo-bar"></htmlCustomElement>'
],
[
'<foo-bar><h2>abc</h2></foo-bar>',
'<htmlCustomElement htmlContent="<h2>abc</h2>" htmlElementName="foo-bar"></htmlCustomElement>'
],
[
'<foo-bar>123<h2>abc</h2></foo-bar>',
'<htmlCustomElement htmlContent="123<h2>abc</h2>" htmlElementName="foo-bar"></htmlCustomElement>'
],
[
'<foo-bar><h2>abc</h2>456</foo-bar>',
'<htmlCustomElement htmlContent="<h2>abc</h2>456" htmlElementName="foo-bar"></htmlCustomElement>'
],
[
'<foo-bar>123<h2>abc</h2>456</foo-bar>',
'<htmlCustomElement htmlContent="123<h2>abc</h2>456" htmlElementName="foo-bar"></htmlCustomElement>'
]
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1342,7 +1342,7 @@ describe( 'TableElementSupport', () => {
expect( getData( editor.model, { withoutSelection: true } ) ).to.equal(
'<paragraph>' +
'<htmlCustomElement ' +
'htmlContent="<custom-element><table dir="ltr"><tbody><tr><td>Foo</td></tr></tbody></table></custom-element>" ' +
'htmlContent="<table dir="ltr"><tbody><tr><td>Foo</td></tr></tbody></table>" ' +
'htmlElementName="custom-element"' +
'>' +
'</htmlCustomElement>' +
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<div id="editor">
<h2>The <code>&lt;template&gt;</code> element</h2>
<p>As block:</p>
<template id="test1"> aa <h2> Hi </h2> bb </template>
<p>In paragraph: <template id="test2"> aa <b> Hi </b> bb </template> </p>
<div>In container: <template id="test3"> aa <h3> Hi </h3> bb </template> </div>
<p>Other custom element: <foo-bar class="abc"> aa <i> Hi </i> bb </foo-bar> </p>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/* globals console:false, window, document */

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
import Code from '@ckeditor/ckeditor5-basic-styles/src/code';
import CodeBlock from '@ckeditor/ckeditor5-code-block/src/codeblock';
import HorizontalLine from '@ckeditor/ckeditor5-horizontal-line/src/horizontalline';
import HtmlEmbed from '@ckeditor/ckeditor5-html-embed/src/htmlembed';
import LinkImage from '@ckeditor/ckeditor5-link/src/linkimage';
import Strikethrough from '@ckeditor/ckeditor5-basic-styles/src/strikethrough';
import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting';
import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline';
import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload';
import RemoveFormat from '@ckeditor/ckeditor5-remove-format/src/removeformat';

import GeneralHtmlSupport from '../../src/generalhtmlsupport';

ClassicEditor
.create( document.querySelector( '#editor' ), {
plugins: [
ArticlePluginSet, Underline, Strikethrough, Code, CodeBlock, LinkImage,
HtmlEmbed, HorizontalLine, ImageUpload, RemoveFormat, SourceEditing, GeneralHtmlSupport
],
toolbar: [
'sourceEditing',
'|',
'heading',
'|',
'bulletedList', 'numberedList',
'|',
'blockQuote', 'uploadImage', 'insertTable', 'mediaEmbed', 'codeBlock',
'|',
'htmlEmbed',
'|',
'undo', 'redo'
],
htmlSupport: {
allow: [
{
name: /./,
styles: true,
attributes: true,
classes: true
}
]
}
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
## GHS `<template>` element support

0 comments on commit 3388cfc

Please sign in to comment.