Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introducing DataProcessor#registerRawContentElementMatcher() #8529

Merged
merged 9 commits into from
Nov 28, 2020
10 changes: 10 additions & 0 deletions packages/ckeditor5-engine/src/dataprocessor/dataprocessor.jsdoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 #registerRawContentElementMatcher
* @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching all view elements whose content should
* be treated as plain text.
*/
14 changes: 14 additions & 0 deletions packages/ckeditor5-engine/src/dataprocessor/htmldataprocessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
registerRawContentElementMatcher( pattern ) {
this._domConverter.registerRawContentElementMatcher( pattern );
}

/**
* Converts an HTML string to its DOM representation. Returns a document fragment containing nodes parsed from
* the provided data.
Expand Down
14 changes: 14 additions & 0 deletions packages/ckeditor5-engine/src/dataprocessor/xmldataprocessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
registerRawContentElementMatcher( pattern ) {
this._domConverter.registerRawContentElementMatcher( pattern );
}

/**
* Converts an XML string to its DOM representation. Returns a document fragment containing nodes parsed from
* the provided data.
Expand Down
64 changes: 59 additions & 5 deletions packages/ckeditor5-engine/src/view/domconverter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<Node>}
*/
this._encounteredRawContentDomNodes = new WeakSet();
}

/**
Expand Down Expand Up @@ -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 );
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 );
}
Expand Down Expand Up @@ -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 all view elements whose content should
* be treated as a raw data.
*/
registerRawContentElementMatcher( pattern ) {
this._rawContentElementMatcher.add( pattern );
}

/**
* Checks if the given DOM position is a correct place for selection boundary. See {@link #isDomSelectionCorrect}.
*
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Expand All @@ -1111,13 +1158,19 @@ 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 ) );
}

/**
* 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
*/
Expand All @@ -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.
Expand Down
19 changes: 19 additions & 0 deletions packages/ckeditor5-engine/tests/dataprocessor/htmldataprocessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,23 @@ describe( 'HtmlDataProcessor', () => {
expect( dataProcessor.toData( fragment ) ).to.equal( '<p>foo</p><p>bar</p>' );
} );
} );

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

const fragment = dataProcessor.toView(
'<p>foo</p>' +
'<div class="raw">' +
'<!-- 123 -->' +
' abc ' +
'<!-- 456 -->' +
'</div>' +
'<p>bar</p>'
);

expect( stringify( fragment ) ).to.equal( '<p>foo</p><div class="raw"></div><p>bar</p>' );
expect( fragment.getChild( 1 ).getCustomProperty( '$rawContent' ) ).to.equal( '<!-- 123 --> abc <!-- 456 -->' );
} );
} );
} );
19 changes: 19 additions & 0 deletions packages/ckeditor5-engine/tests/dataprocessor/xmldataprocessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,23 @@ describe( 'XmlDataProcessor', () => {
expect( dataProcessor.toData( fragment ) ).to.equal( '<p>foo</p><p>bar</p>' );
} );
} );

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

const fragment = dataProcessor.toView(
'<p>foo</p>' +
'<div class="raw">' +
'<!-- 123 -->' +
' abc ' +
'<!-- 456 -->' +
'</div>' +
'<p>bar</p>'
);

expect( stringify( fragment ) ).to.equal( '<p>foo</p><div class="raw"></div><p>bar</p>' );
expect( fragment.getChild( 1 ).getCustomProperty( '$rawContent' ) ).to.equal( '<!-- 123 --> abc <!-- 456 -->' );
} );
} );
} );
56 changes: 56 additions & 0 deletions packages/ckeditor5-engine/tests/view/domconverter/dom-to-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,42 @@ describe( 'DomConverter', () => {
expect( viewFragment2 ).to.equal( viewFragment );
} );

it( 'should assign $rawContent custom property for view elements registered as raw content elements', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too few tests IMO. Right now it tests only one pattern. I think that we could check a bit more:

  • multiple patterns registered
  • what if div.raw-content-container has other attributes, classed or styles, ie div[ data-foo="bar"].raw-content-container

converter.registerRawContentElementMatcher( {
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( '<!-- foo --><img>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 return null for block filler', () => {
// eslint-disable-next-line new-cap
const domFiller = BR_FILLER( document );
Expand Down Expand Up @@ -641,6 +677,26 @@ describe( 'DomConverter', () => {
expect( viewP.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foo ' );
} );

it( 'not before or after raw content inline element', () => {
converter.registerRawContentElementMatcher( {
name: 'span'
} );

const domP = createElement( document, 'p', {}, [
document.createTextNode( ' foo ' ),
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( 2 ).data ).to.equal( ' bar' );
} );

//
// See also whitespace-handling-integration.js.
//
Expand Down
20 changes: 10 additions & 10 deletions packages/ckeditor5-html-embed/src/htmlembedediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -94,22 +92,24 @@ 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.registerRawContentElementMatcher( {
name: 'div',
classes: 'raw-html-embed'
} );

editor.conversion.for( 'upcast' ).elementToElement( {
view: {
name: 'div',
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' )
} );
}
} );
Expand Down
22 changes: 22 additions & 0 deletions packages/ckeditor5-html-embed/tests/htmlembedediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,28 @@ describe( 'HtmlEmbedEditing', () => {
'</div>'
);
} );

it( 'should convert innerHTML (and preserve comments and raw data formatting) of div.raw-html-embed', () => {
const rawContent = [
' <!-- foo -->',
' <p>',
' <b>Foo B.</b>',
' <!-- abc -->',
' <i>Foo I.</i>',
' </p>',
' <!-- bar -->'
].join( '\n' );

editor.setData(
'<div class="raw-html-embed">' +
rawContent +
'</div>'
);

const rawHtml = model.document.getRoot().getChild( 0 );

expect( rawHtml.getAttribute( 'value' ) ).to.equal( rawContent );
} );
} );
} );

Expand Down
Loading