diff --git a/packages/ckeditor5-engine/src/dataprocessor/dataprocessor.jsdoc b/packages/ckeditor5-engine/src/dataprocessor/dataprocessor.jsdoc index 520a66a04dc..86d2b6282bf 100644 --- a/packages/ckeditor5-engine/src/dataprocessor/dataprocessor.jsdoc +++ b/packages/ckeditor5-engine/src/dataprocessor/dataprocessor.jsdoc @@ -38,3 +38,13 @@ * @param {*} data The data to be processed. * @returns {module:engine/view/documentfragment~DocumentFragment} */ + +/** + * Registers a {@link module:engine/view/matcher~MatcherPattern} for view elements whose content should be treated as a raw data + * and it's content should be converted to {@link module:engine/view/element~Element#getCustomProperty view element custom property} + * `"$rawContent"` while converting {@link #toView to view}. + * + * @method #registerRawContentMatcher + * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching all view elements whose content should + * be treated as plain text. + */ diff --git a/packages/ckeditor5-engine/src/dataprocessor/htmldataprocessor.js b/packages/ckeditor5-engine/src/dataprocessor/htmldataprocessor.js index 1b8efa6dedf..7b48b0e23dc 100644 --- a/packages/ckeditor5-engine/src/dataprocessor/htmldataprocessor.js +++ b/packages/ckeditor5-engine/src/dataprocessor/htmldataprocessor.js @@ -79,6 +79,20 @@ export default class HtmlDataProcessor { return this._domConverter.domToView( domFragment ); } + /** + * Registers a {@link module:engine/view/matcher~MatcherPattern} for view elements whose content should be treated as a raw data + * and not processed during conversion from DOM to view elements. + * + * The raw data can be later accessed by {@link module:engine/view/element~Element#getCustomProperty view element custom property} + * `"$rawContent"`. + * + * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching all view elements whose content should + * be treated as a raw data. + */ + registerRawContentMatcher( pattern ) { + this._domConverter.registerRawContentMatcher( pattern ); + } + /** * Converts an HTML string to its DOM representation. Returns a document fragment containing nodes parsed from * the provided data. diff --git a/packages/ckeditor5-engine/src/dataprocessor/xmldataprocessor.js b/packages/ckeditor5-engine/src/dataprocessor/xmldataprocessor.js index 8f0c03cd462..0ba6373ada4 100644 --- a/packages/ckeditor5-engine/src/dataprocessor/xmldataprocessor.js +++ b/packages/ckeditor5-engine/src/dataprocessor/xmldataprocessor.js @@ -96,6 +96,20 @@ export default class XmlDataProcessor { return this._domConverter.domToView( domFragment, { keepOriginalCase: true } ); } + /** + * Registers a {@link module:engine/view/matcher~MatcherPattern} for view elements whose content should be treated as a raw data + * and not processed during conversion from XML to view elements. + * + * The raw data can be later accessed by {@link module:engine/view/element~Element#getCustomProperty view element custom property} + * `"$rawContent"`. + * + * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching all view elements whose content should + * be treated as a raw data. + */ + registerRawContentMatcher( pattern ) { + this._domConverter.registerRawContentMatcher( pattern ); + } + /** * Converts an XML string to its DOM representation. Returns a document fragment containing nodes parsed from * the provided data. diff --git a/packages/ckeditor5-engine/src/view/domconverter.js b/packages/ckeditor5-engine/src/view/domconverter.js index f3d8beab359..2814eb4bac8 100644 --- a/packages/ckeditor5-engine/src/view/domconverter.js +++ b/packages/ckeditor5-engine/src/view/domconverter.js @@ -16,6 +16,7 @@ import ViewRange from './range'; import ViewSelection from './selection'; import ViewDocumentFragment from './documentfragment'; import ViewTreeWalker from './treewalker'; +import Matcher from './matcher'; import { BR_FILLER, getDataWithoutFiller, INLINE_FILLER_LENGTH, isInlineFiller, NBSP_FILLER, startsWithFiller } from './filler'; import global from '@ckeditor/ckeditor5-utils/src/dom/global'; @@ -118,6 +119,23 @@ export default class DomConverter { * @member {WeakMap} module:engine/view/domconverter~DomConverter#_fakeSelectionMapping */ this._fakeSelectionMapping = new WeakMap(); + + /** + * Matcher for view elements whose content should be treated as a raw data + * and not processed during conversion from DOM nodes to view elements. + * + * @private + * @type {module:engine/view/matcher~Matcher} + */ + this._rawContentElementMatcher = new Matcher(); + + /** + * Set of encountered raw content DOM nodes. It is used for preventing left trimming of the following text node. + * + * @private + * @type {WeakSet.} + */ + this._encounteredRawContentDomNodes = new WeakSet(); } /** @@ -253,7 +271,7 @@ export default class DomConverter { } } - if ( options.withChildren || options.withChildren === undefined ) { + if ( options.withChildren !== false ) { for ( const child of this.viewChildrenToDom( viewNode, domDocument, options ) ) { domElement.appendChild( child ); } @@ -400,7 +418,7 @@ export default class DomConverter { } // When node is inside a UIElement or a RawElement return that parent as it's view representation. - const hostElement = this.getHostViewElement( domNode, this._domToViewMapping ); + const hostElement = this.getHostViewElement( domNode ); if ( hostElement ) { return hostElement; @@ -445,9 +463,19 @@ export default class DomConverter { for ( let i = attrs.length - 1; i >= 0; i-- ) { viewElement._setAttribute( attrs[ i ].name, attrs[ i ].value ); } + + // Treat this element's content as a raw data if it was registered as such. + if ( options.withChildren !== false && this._rawContentElementMatcher.match( viewElement ) ) { + viewElement._setCustomProperty( '$rawContent', domNode.innerHTML ); + + // Store a DOM node to prevent left trimming of the following text node. + this._encounteredRawContentDomNodes.add( domNode ); + + return viewElement; + } } - if ( options.withChildren || options.withChildren === undefined ) { + if ( options.withChildren !== false ) { for ( const child of this.domChildrenToView( domNode, options ) ) { viewElement._appendChild( child ); } @@ -911,6 +939,23 @@ export default class DomConverter { this._isDomSelectionPositionCorrect( domSelection.focusNode, domSelection.focusOffset ); } + /** + * Registers a {@link module:engine/view/matcher~MatcherPattern} for view elements whose content should be treated as a raw data + * and not processed during conversion from DOM nodes to view elements. + * + * This is affecting how {@link module:engine/view/domconverter~DomConverter#domToView} and + * {@link module:engine/view/domconverter~DomConverter#domChildrenToView} processes DOM nodes. + * + * The raw data can be later accessed by {@link module:engine/view/element~Element#getCustomProperty view element custom property} + * `"$rawContent"`. + * + * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching view element which content should + * be treated as a raw data. + */ + registerRawContentMatcher( pattern ) { + this._rawContentElementMatcher.add( pattern ); + } + /** * Checks if the given DOM position is a correct place for selection boundary. See {@link #isDomSelectionCorrect}. * @@ -1051,7 +1096,7 @@ export default class DomConverter { const prevNode = this._getTouchingInlineDomNode( node, false ); const nextNode = this._getTouchingInlineDomNode( node, true ); - const shouldLeftTrim = this._checkShouldLeftTrimDomText( prevNode ); + const shouldLeftTrim = this._checkShouldLeftTrimDomText( node, prevNode ); const shouldRightTrim = this._checkShouldRightTrimDomText( node, nextNode ); // If the previous dom text node does not exist or it ends by whitespace character, remove space character from the beginning @@ -1100,9 +1145,11 @@ export default class DomConverter { * Helper function which checks if a DOM text node, preceded by the given `prevNode` should * be trimmed from the left side. * + * @private + * @param {Node} node * @param {Node} prevNode */ - _checkShouldLeftTrimDomText( prevNode ) { + _checkShouldLeftTrimDomText( node, prevNode ) { if ( !prevNode ) { return true; } @@ -1111,6 +1158,11 @@ export default class DomConverter { return true; } + // Shouldn't left trim if previous node is a node that was encountered as a raw content node. + if ( this._encounteredRawContentDomNodes.has( node.previousSibling ) ) { + return false; + } + return /[^\S\u00A0]/.test( prevNode.data.charAt( prevNode.data.length - 1 ) ); } @@ -1118,6 +1170,7 @@ export default class DomConverter { * Helper function which checks if a DOM text node, succeeded by the given `nextNode` should * be trimmed from the right side. * + * @private * @param {Node} node * @param {Node} nextNode */ @@ -1133,6 +1186,7 @@ export default class DomConverter { * Helper function. For given {@link module:engine/view/text~Text view text node}, it finds previous or next sibling * that is contained in the same container element. If there is no such sibling, `null` is returned. * + * @private * @param {module:engine/view/text~Text} node Reference node. * @param {Boolean} getNext * @returns {module:engine/view/text~Text|null} Touching text node or `null` if there is no next or previous touching text node. diff --git a/packages/ckeditor5-engine/tests/dataprocessor/htmldataprocessor.js b/packages/ckeditor5-engine/tests/dataprocessor/htmldataprocessor.js index 644b8f6d477..3e98fea86bc 100644 --- a/packages/ckeditor5-engine/tests/dataprocessor/htmldataprocessor.js +++ b/packages/ckeditor5-engine/tests/dataprocessor/htmldataprocessor.js @@ -114,4 +114,23 @@ describe( 'HtmlDataProcessor', () => { expect( dataProcessor.toData( fragment ) ).to.equal( '

foo

bar

' ); } ); } ); + + describe( 'registerRawContentMatcher()', () => { + it( 'should handle elements matching to MatcherPattern as elements with raw content', () => { + dataProcessor.registerRawContentMatcher( { name: 'div', classes: 'raw' } ); + + const fragment = dataProcessor.toView( + '

foo

' + + '
' + + '' + + ' abc ' + + '' + + '
' + + '

bar

' + ); + + expect( stringify( fragment ) ).to.equal( '

foo

bar

' ); + expect( fragment.getChild( 1 ).getCustomProperty( '$rawContent' ) ).to.equal( ' abc ' ); + } ); + } ); } ); diff --git a/packages/ckeditor5-engine/tests/dataprocessor/xmldataprocessor.js b/packages/ckeditor5-engine/tests/dataprocessor/xmldataprocessor.js index b262eb4b849..f01d9f47da9 100644 --- a/packages/ckeditor5-engine/tests/dataprocessor/xmldataprocessor.js +++ b/packages/ckeditor5-engine/tests/dataprocessor/xmldataprocessor.js @@ -103,4 +103,23 @@ describe( 'XmlDataProcessor', () => { expect( dataProcessor.toData( fragment ) ).to.equal( '

foo

bar

' ); } ); } ); + + describe( 'registerRawContentMatcher()', () => { + it( 'should handle elements matching to MatcherPattern as elements with raw content', () => { + dataProcessor.registerRawContentMatcher( { name: 'div', classes: 'raw' } ); + + const fragment = dataProcessor.toView( + '

foo

' + + '
' + + '' + + ' abc ' + + '' + + '
' + + '

bar

' + ); + + expect( stringify( fragment ) ).to.equal( '

foo

bar

' ); + expect( fragment.getChild( 1 ).getCustomProperty( '$rawContent' ) ).to.equal( ' abc ' ); + } ); + } ); } ); diff --git a/packages/ckeditor5-engine/tests/view/domconverter/rawcontent.js b/packages/ckeditor5-engine/tests/view/domconverter/rawcontent.js new file mode 100644 index 00000000000..9ed21e5787e --- /dev/null +++ b/packages/ckeditor5-engine/tests/view/domconverter/rawcontent.js @@ -0,0 +1,246 @@ +/** + * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals document */ + +import DomConverter from '../../../src/view/domconverter'; +import ViewDocument from '../../../src/view/document'; +import ViewElement from '../../../src/view/element'; +import { StylesProcessor } from '../../../src/view/stylesmap'; + +import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement'; + +describe( 'DOMConverter raw content matcher', () => { + let converter, viewDocument; + + beforeEach( () => { + viewDocument = new ViewDocument( new StylesProcessor() ); + converter = new DomConverter( viewDocument ); + } ); + + describe( 'domToView()', () => { + describe( 'assign $rawContent custom property for view elements registered as raw content elements', () => { + it( 'should handle exact match of an element name and classes', () => { + converter.registerRawContentMatcher( { + name: 'div', + classes: 'raw-content-container' + } ); + + const domDiv = createElement( document, 'div', {}, [ + createElement( document, 'img' ), + createElement( document, 'div', { 'class': 'raw-content-container' }, [ + document.createComment( ' foo ' ), + createElement( document, 'img' ), + document.createTextNode( 'bar\n123' ) + ] ), + createElement( document, 'div', {}, [ + document.createComment( 'foo' ), + createElement( document, 'img' ), + document.createTextNode( 'bar\n123' ) + ] ), + document.createTextNode( 'abc' ) + ] ); + + const viewDiv = converter.domToView( domDiv ); + + expect( viewDiv ).to.be.an.instanceof( ViewElement ); + expect( viewDiv.name ).to.equal( 'div' ); + + expect( viewDiv.childCount ).to.equal( 4 ); + expect( viewDiv.getChild( 0 ).name ).to.equal( 'img' ); + expect( viewDiv.getChild( 1 ).getCustomProperty( '$rawContent' ) ).to.equal( 'bar\n123' ); + expect( viewDiv.getChild( 2 ).getCustomProperty( '$rawContent' ) ).to.be.undefined; + expect( viewDiv.getChild( 2 ).childCount ).to.equal( 2 ); + expect( viewDiv.getChild( 2 ).getChild( 0 ).name ).to.equal( 'img' ); + expect( viewDiv.getChild( 2 ).getChild( 1 ).data ).to.equal( 'bar 123' ); + expect( viewDiv.getChild( 3 ).data ).to.equal( 'abc' ); + } ); + + it( 'should handle elements with more classes, styles and attributes not required by the matcher pattern', () => { + converter.registerRawContentMatcher( { + name: 'div', + classes: 'raw-content-container' + } ); + + const domDiv = createElement( document, 'div', {}, [ + createElement( document, 'img' ), + createElement( document, 'div', { + 'class': 'raw-content-container', + 'style': 'border: 1px solid red', + 'data-foo': 'bar' + }, [ + document.createComment( ' foo ' ), + createElement( document, 'img' ), + document.createTextNode( 'bar\n123' ) + ] ), + createElement( document, 'div', {}, [ + document.createComment( 'foo' ), + createElement( document, 'img' ), + document.createTextNode( 'bar\n123' ) + ] ), + document.createTextNode( 'abc' ) + ] ); + + const viewDiv = converter.domToView( domDiv ); + + expect( viewDiv ).to.be.an.instanceof( ViewElement ); + expect( viewDiv.name ).to.equal( 'div' ); + + expect( viewDiv.childCount ).to.equal( 4 ); + expect( viewDiv.getChild( 0 ).name ).to.equal( 'img' ); + expect( viewDiv.getChild( 1 ).getCustomProperty( '$rawContent' ) ).to.equal( 'bar\n123' ); + expect( viewDiv.getChild( 2 ).getCustomProperty( '$rawContent' ) ).to.be.undefined; + expect( viewDiv.getChild( 2 ).childCount ).to.equal( 2 ); + expect( viewDiv.getChild( 2 ).getChild( 0 ).name ).to.equal( 'img' ); + expect( viewDiv.getChild( 2 ).getChild( 1 ).data ).to.equal( 'bar 123' ); + expect( viewDiv.getChild( 3 ).data ).to.equal( 'abc' ); + } ); + + it( 'should handle multiple matchers (but nested ones should not be matched)', () => { + converter.registerRawContentMatcher( { + name: 'div', + classes: 'raw-content-container' + } ); + + converter.registerRawContentMatcher( { + name: 'span', + attributes: { + 'data-foo': 'bar' + } + } ); + + const domDiv = createElement( document, 'div', {}, [ + createElement( document, 'img' ), + createElement( document, 'div', { + 'class': 'raw-content-container', + 'style': 'border: 1px solid red', + 'data-foo': 'bar' + }, [ + document.createComment( ' foo ' ), + createElement( document, 'img' ), + createElement( document, 'span', { + 'data-foo': 'bar' + }, [ + document.createTextNode( 'nested span' ) + ] ), + document.createTextNode( 'bar\n123' ) + ] ), + createElement( document, 'div', {}, [ + document.createComment( 'foo' ), + createElement( document, 'img' ), + document.createTextNode( 'bar\n123' ) + ] ), + createElement( document, 'span', { 'data-foo': 'bar' }, 'some span' ), + createElement( document, 'span', {}, 'other span' ), + document.createTextNode( 'abc' ) + ] ); + + const viewDiv = converter.domToView( domDiv ); + + expect( viewDiv ).to.be.an.instanceof( ViewElement ); + expect( viewDiv.name ).to.equal( 'div' ); + + expect( viewDiv.childCount ).to.equal( 6 ); + expect( viewDiv.getChild( 0 ).name ).to.equal( 'img' ); + expect( viewDiv.getChild( 1 ).getCustomProperty( '$rawContent' ) ).to.equal( + 'nested spanbar\n123' + ); + expect( viewDiv.getChild( 2 ).getCustomProperty( '$rawContent' ) ).to.be.undefined; + expect( viewDiv.getChild( 2 ).childCount ).to.equal( 2 ); + expect( viewDiv.getChild( 2 ).getChild( 0 ).name ).to.equal( 'img' ); + expect( viewDiv.getChild( 2 ).getChild( 1 ).data ).to.equal( 'bar 123' ); + expect( viewDiv.getChild( 3 ).getCustomProperty( '$rawContent' ) ).to.equal( 'some span' ); + expect( viewDiv.getChild( 4 ).name ).to.equal( 'span' ); + expect( viewDiv.getChild( 4 ).getChild( 0 ).data ).to.equal( 'other span' ); + expect( viewDiv.getChild( 5 ).data ).to.equal( 'abc' ); + } ); + + it( 'should handle elements by an attribute or class only', () => { + converter.registerRawContentMatcher( { + classes: 'raw-content-container' + } ); + + converter.registerRawContentMatcher( { + attributes: { + 'data-foo': 'bar' + } + } ); + + const domDiv = createElement( document, 'div', {}, [ + createElement( document, 'img' ), + createElement( document, 'div', { + 'class': 'raw-content-container' + }, [ + document.createComment( ' foo ' ), + createElement( document, 'img' ), + document.createTextNode( 'bar\n123' ) + ] ), + createElement( document, 'div', { + 'data-foo': 'bar' + }, [ + document.createComment( 'bar' ), + document.createTextNode( '123' ) + ] ), + document.createTextNode( 'abc' ) + ] ); + + const viewDiv = converter.domToView( domDiv ); + + expect( viewDiv ).to.be.an.instanceof( ViewElement ); + expect( viewDiv.name ).to.equal( 'div' ); + + expect( viewDiv.childCount ).to.equal( 4 ); + expect( viewDiv.getChild( 0 ).name ).to.equal( 'img' ); + expect( viewDiv.getChild( 1 ).getCustomProperty( '$rawContent' ) ).to.equal( 'bar\n123' ); + expect( viewDiv.getChild( 2 ).getCustomProperty( '$rawContent' ) ).to.equal( '123' ); + expect( viewDiv.getChild( 3 ).data ).to.equal( 'abc' ); + } ); + } ); + + describe( 'whitespace trimming', () => { + it( 'should not trim whitespaces before or after raw content inline element', () => { + converter.registerRawContentMatcher( { + name: 'span' + } ); + + const domP = createElement( document, 'p', {}, [ + document.createTextNode( ' foo ' ), + createElement( document, 'span', {}, ' abc ' ), + document.createTextNode( ' bar ' ) + ] ); + + const viewP = converter.domToView( domP ); + + expect( viewP.childCount ).to.equal( 3 ); + expect( viewP.getChild( 0 ).data ).to.equal( 'foo ' ); + expect( viewP.getChild( 1 ).getCustomProperty( '$rawContent' ) ).to.equal( ' abc ' ); + expect( viewP.getChild( 2 ).data ).to.equal( ' bar' ); + } ); + + it( 'should not trim whitespaces before or after raw content inline element with deeper nesting', () => { + converter.registerRawContentMatcher( { + name: 'span' + } ); + + const domP = createElement( document, 'p', {}, [ + document.createTextNode( ' foo ' ), + createElement( document, 'span', {}, [ + createElement( document, 'span', {}, [ + document.createTextNode( ' abc ' ) + ] ) + ] ), + document.createTextNode( ' bar ' ) + ] ); + + const viewP = converter.domToView( domP ); + + expect( viewP.childCount ).to.equal( 3 ); + expect( viewP.getChild( 0 ).data ).to.equal( 'foo ' ); + expect( viewP.getChild( 1 ).getCustomProperty( '$rawContent' ) ).to.equal( ' abc ' ); + expect( viewP.getChild( 2 ).data ).to.equal( ' bar' ); + } ); + } ); + } ); +} ); diff --git a/packages/ckeditor5-html-embed/src/htmlembedediting.js b/packages/ckeditor5-html-embed/src/htmlembedediting.js index c0354f95cbf..89ba7e8bc37 100644 --- a/packages/ckeditor5-html-embed/src/htmlembedediting.js +++ b/packages/ckeditor5-html-embed/src/htmlembedediting.js @@ -8,8 +8,6 @@ */ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; -import HtmlDataProcessor from '@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor'; -import UpcastWriter from '@ckeditor/ckeditor5-engine/src/view/upcastwriter'; import { logWarning } from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import { toWidget } from '@ckeditor/ckeditor5-widget/src/utils'; import InsertHtmlEmbedCommand from './inserthtmlembedcommand'; @@ -94,8 +92,13 @@ export default class HtmlEmbedEditing extends Plugin { const view = editor.editing.view; const htmlEmbedConfig = editor.config.get( 'htmlEmbed' ); - const upcastWriter = new UpcastWriter( view.document ); - const htmlProcessor = new HtmlDataProcessor( view.document ); + + // Register div.raw-html-embed as a raw content element so all of it's content will be provided + // as a view element's custom property while data upcasting. + editor.data.processor.registerRawContentMatcher( { + name: 'div', + classes: 'raw-html-embed' + } ); editor.conversion.for( 'upcast' ).elementToElement( { view: { @@ -103,13 +106,10 @@ export default class HtmlEmbedEditing extends Plugin { classes: 'raw-html-embed' }, model: ( viewElement, { writer } ) => { - // Note: The below line has a side-effect – the children are *moved* to the DF so - // viewElement becomes empty. It's fine here. - const fragment = upcastWriter.createDocumentFragment( viewElement.getChildren() ); - const innerHtml = htmlProcessor.toData( fragment ); - + // The div.raw-html-embed is registered as a raw content element, + // so all it's content is available in a custom property. return writer.createElement( 'rawHtml', { - value: innerHtml + value: viewElement.getCustomProperty( '$rawContent' ) } ); } } ); diff --git a/packages/ckeditor5-html-embed/tests/htmlembedediting.js b/packages/ckeditor5-html-embed/tests/htmlembedediting.js index 85ea6112919..673970699c6 100644 --- a/packages/ckeditor5-html-embed/tests/htmlembedediting.js +++ b/packages/ckeditor5-html-embed/tests/htmlembedediting.js @@ -225,6 +225,28 @@ describe( 'HtmlEmbedEditing', () => { '' ); } ); + + it( 'should convert innerHTML (and preserve comments and raw data formatting) of div.raw-html-embed', () => { + const rawContent = [ + ' ', + '

', + ' Foo B.', + ' ', + ' Foo I.', + '

', + ' ' + ].join( '\n' ); + + editor.setData( + '
' + + rawContent + + '
' + ); + + const rawHtml = model.document.getRoot().getChild( 0 ); + + expect( rawHtml.getAttribute( 'value' ) ).to.equal( rawContent ); + } ); } ); } ); diff --git a/packages/ckeditor5-html-embed/tests/manual/htmlembed.html b/packages/ckeditor5-html-embed/tests/manual/htmlembed.html index 65c0de67891..2e3677d1ce0 100644 --- a/packages/ckeditor5-html-embed/tests/manual/htmlembed.html +++ b/packages/ckeditor5-html-embed/tests/manual/htmlembed.html @@ -154,4 +154,11 @@

<table> tag as HTML snippet (code coverage report)

+ +

HTML comments in HTML snippet

+
+ +
fooo
+ +
diff --git a/packages/ckeditor5-markdown-gfm/src/gfmdataprocessor.js b/packages/ckeditor5-markdown-gfm/src/gfmdataprocessor.js index fd21b8eeeb4..54333133105 100644 --- a/packages/ckeditor5-markdown-gfm/src/gfmdataprocessor.js +++ b/packages/ckeditor5-markdown-gfm/src/gfmdataprocessor.js @@ -69,4 +69,18 @@ export default class GFMDataProcessor { const html = this._htmlDP.toData( viewFragment ); return html2markdown( html ); } + + /** + * Registers a {@link module:engine/view/matcher~MatcherPattern} for view elements whose content should be treated as a raw data + * and not processed during conversion from Markdown to view elements. + * + * The raw data can be later accessed by {@link module:engine/view/element~Element#getCustomProperty view element custom property} + * `"$rawContent"`. + * + * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching all view elements whose content should + * be treated as a raw data. + */ + registerRawContentMatcher( pattern ) { + this._htmlDP.registerRawContentMatcher( pattern ); + } } diff --git a/packages/ckeditor5-markdown-gfm/tests/_utils/utils.js b/packages/ckeditor5-markdown-gfm/tests/_utils/utils.js index f7b4043265e..32883c44552 100644 --- a/packages/ckeditor5-markdown-gfm/tests/_utils/utils.js +++ b/packages/ckeditor5-markdown-gfm/tests/_utils/utils.js @@ -17,6 +17,7 @@ import { StylesProcessor } from '@ckeditor/ckeditor5-engine/src/view/stylesmap'; * @param {Object} [options] Additional options. * @param {Function} [options.setup] A function that receives the data processor instance before its execution. * markdown string (which will be used if this parameter is not provided). + * @returns {module:engine/view/documentfragment~DocumentFragment} */ export function testDataProcessor( markdown, viewString, normalizedMarkdown, options ) { const viewDocument = new ViewDocument( new StylesProcessor() ); @@ -37,6 +38,8 @@ export function testDataProcessor( markdown, viewString, normalizedMarkdown, opt const normalized = typeof normalizedMarkdown !== 'undefined' ? normalizedMarkdown : markdown; expect( cleanMarkdown( dataProcessor.toData( viewFragment ) ) ).to.equal( normalized ); + + return viewFragment; } function cleanHtml( html ) { diff --git a/packages/ckeditor5-markdown-gfm/tests/gfmdataprocessor/code.js b/packages/ckeditor5-markdown-gfm/tests/gfmdataprocessor/code.js index df6da3d8e19..b1ac86f2f8c 100644 --- a/packages/ckeditor5-markdown-gfm/tests/gfmdataprocessor/code.js +++ b/packages/ckeditor5-markdown-gfm/tests/gfmdataprocessor/code.js @@ -312,5 +312,36 @@ describe( 'GFMDataProcessor', () => { '' ); } ); + + it( 'should support #registerRawContentMatcher()', () => { + const viewFragment = testDataProcessor( + [ + '```raw', + 'var a = \'hello\';', + 'console.log(a + \' world\');', + '```' + ].join( '\n' ), + + '
', + + '', + + { + setup( processor ) { + processor.registerRawContentMatcher( { + name: 'code', + classes: 'language-raw' + } ); + } + } + ); + + expect( viewFragment.getChild( 0 ).getChild( 0 ).getCustomProperty( '$rawContent' ) ).to.equal( + [ + 'var a = \'hello\';', + 'console.log(a + \' world\');' + ].join( '\n' ) + ); + } ); } ); } );