diff --git a/packages/ckeditor5-engine/src/dataprocessor/dataprocessor.jsdoc b/packages/ckeditor5-engine/src/dataprocessor/dataprocessor.jsdoc index 673ef7d8595..a116ceed5a2 100644 --- a/packages/ckeditor5-engine/src/dataprocessor/dataprocessor.jsdoc +++ b/packages/ckeditor5-engine/src/dataprocessor/dataprocessor.jsdoc @@ -49,3 +49,16 @@ * @param {module:engine/view/matcher~MatcherPattern} pattern Pattern matching all view elements whose content should * be treated as plain text. */ + +/** + * If the processor is set to use marked fillers, it will insert nbsp fillers wrapped in spans + * (` `), instead of regular nbsp characters (` `). + * + * This mode allows for more precise handling of block fillers (so they don't leak into editor content) but bloats the editor + * data with additional markup. + * + * This mode may be required by some features and will be turned on by them automatically. + * + * @method #useFillerType + * @param {'default'|'marked'} type Whether to use default or marked nbsp block fillers. + */ diff --git a/packages/ckeditor5-engine/src/dataprocessor/htmldataprocessor.js b/packages/ckeditor5-engine/src/dataprocessor/htmldataprocessor.js index f0d50fde4d5..4b1de28b9f2 100644 --- a/packages/ckeditor5-engine/src/dataprocessor/htmldataprocessor.js +++ b/packages/ckeditor5-engine/src/dataprocessor/htmldataprocessor.js @@ -93,6 +93,21 @@ export default class HtmlDataProcessor { this._domConverter.registerRawContentMatcher( pattern ); } + /** + * If the processor is set to use marked fillers, it will insert nbsp fillers wrapped in spans + * (` `), instead of regular nbsp characters (` `). + * + * This mode allows for more precise handling of block fillers (so they don't leak into editor content) but bloats the editor + * data with additional markup. + * + * This mode may be required by some features and will be turned on by them automatically. + * + * @param {'default'|'marked'} type Whether to use default or marked nbsp block fillers. + */ + useFillerType( type ) { + this._domConverter.blockFillerMode = type == 'marked' ? 'markedNbsp' : 'nbsp'; + } + /** * 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 c2162788dc6..141dbef9925 100644 --- a/packages/ckeditor5-engine/src/dataprocessor/xmldataprocessor.js +++ b/packages/ckeditor5-engine/src/dataprocessor/xmldataprocessor.js @@ -110,6 +110,21 @@ export default class XmlDataProcessor { this._domConverter.registerRawContentMatcher( pattern ); } + /** + * If the processor is set to use marked fillers, it will insert nbsp fillers wrapped in spans + * (` `), instead of regular nbsp characters (` `). + * + * This mode allows for more precise handling of block fillers (so they don't leak into editor content) but bloats the editor + * data with additional markup. + * + * This mode may be required by some features and will be turned on by them automatically. + * + * @param {'default'|'marked'} type Whether to use default or marked nbsp block fillers. + */ + useFillerType( type ) { + this._domConverter.blockFillerMode = type == 'marked' ? 'markedNbsp' : 'nbsp'; + } + /** * 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 e263bb9d41a..5e4e3b86b99 100644 --- a/packages/ckeditor5-engine/src/view/domconverter.js +++ b/packages/ckeditor5-engine/src/view/domconverter.js @@ -17,7 +17,10 @@ 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 { + BR_FILLER, INLINE_FILLER_LENGTH, NBSP_FILLER, MARKED_NBSP_FILLER, + getDataWithoutFiller, isInlineFiller, startsWithFiller +} from './filler'; import global from '@ckeditor/ckeditor5-utils/src/dom/global'; import indexOf from '@ckeditor/ckeditor5-utils/src/dom/indexof'; @@ -26,8 +29,9 @@ import getCommonAncestor from '@ckeditor/ckeditor5-utils/src/dom/getcommonancest import isText from '@ckeditor/ckeditor5-utils/src/dom/istext'; import { isElement } from 'lodash-es'; -// eslint-disable-next-line new-cap -const BR_FILLER_REF = BR_FILLER( document ); +const BR_FILLER_REF = BR_FILLER( document ); // eslint-disable-line new-cap +const NBSP_FILLER_REF = NBSP_FILLER( document ); // eslint-disable-line new-cap +const MARKED_NBSP_FILLER_REF = MARKED_NBSP_FILLER( document ); // eslint-disable-line new-cap /** * `DomConverter` is a set of tools to do transformations between DOM nodes and view nodes. It also handles @@ -60,8 +64,7 @@ export default class DomConverter { /** * The mode of a block filler used by the DOM converter. * - * @readonly - * @member {'br'|'nbsp'} module:engine/view/domconverter~DomConverter#blockFillerMode + * @member {'br'|'nbsp'|'markedNbsp'} module:engine/view/domconverter~DomConverter#blockFillerMode */ this.blockFillerMode = options.blockFillerMode || 'br'; @@ -86,16 +89,6 @@ export default class DomConverter { */ this.blockElements = [ 'p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'dd', 'dt', 'figcaption', 'td', 'th' ]; - /** - * Block {@link module:engine/view/filler filler} creator, which is used to create all block fillers during the - * view-to-DOM conversion and to recognize block fillers during the DOM-to-view conversion. - * - * @readonly - * @private - * @member {Function} module:engine/view/domconverter~DomConverter#_blockFiller - */ - this._blockFiller = this.blockFillerMode == 'br' ? BR_FILLER : NBSP_FILLER; - /** * The DOM-to-view mapping. * @@ -297,7 +290,7 @@ export default class DomConverter { for ( const childView of viewElement.getChildren() ) { if ( fillerPositionOffset === offset ) { - yield this._blockFiller( domDocument ); + yield this._getBlockFiller( domDocument ); } yield this.viewToDom( childView, domDocument, options ); @@ -306,7 +299,7 @@ export default class DomConverter { } if ( fillerPositionOffset === offset ) { - yield this._blockFiller( domDocument ); + yield this._getBlockFiller( domDocument ); } } @@ -413,7 +406,7 @@ export default class DomConverter { * or `null` if DOM node is a {@link module:engine/view/filler filler} or the given node is an empty text node. */ domToView( domNode, options = {} ) { - if ( this.isBlockFiller( domNode, this.blockFillerMode ) ) { + if ( this.isBlockFiller( domNode ) ) { return null; } @@ -581,7 +574,7 @@ export default class DomConverter { * @returns {module:engine/view/position~Position} viewPosition View position. */ domPositionToView( domParent, domOffset ) { - if ( this.isBlockFiller( domParent, this.blockFillerMode ) ) { + if ( this.isBlockFiller( domParent ) ) { return this.domPositionToView( domParent.parentNode, indexOf( domParent ) ); } @@ -863,13 +856,13 @@ export default class DomConverter { return domNode.isEqualNode( BR_FILLER_REF ); } - // Special case for
' ); + + dataProcessor.useFillerType( 'marked' ); + + expect( dataProcessor.toData( fragment ) ).to.equal( '
' ); + + dataProcessor.useFillerType( 'default' ); + + expect( dataProcessor.toData( fragment ) ).to.equal( '
' ); + } ); + } ); } ); diff --git a/packages/ckeditor5-engine/tests/dataprocessor/xmldataprocessor.js b/packages/ckeditor5-engine/tests/dataprocessor/xmldataprocessor.js index eeb043b6510..824b74cec79 100644 --- a/packages/ckeditor5-engine/tests/dataprocessor/xmldataprocessor.js +++ b/packages/ckeditor5-engine/tests/dataprocessor/xmldataprocessor.js @@ -122,4 +122,20 @@ describe( 'XmlDataProcessor', () => { expect( fragment.getChild( 1 ).getCustomProperty( '$rawContent' ) ).to.equal( ' abc ' ); } ); } ); + + describe( 'useFillerType()', () => { + it( 'should turn on and off using marked block fillers', () => { + const fragment = parse( '
' ); + + dataProcessor.useFillerType( 'marked' ); + + expect( dataProcessor.toData( fragment ) ).to.equal( '
' ); + + dataProcessor.useFillerType( 'default' ); + + expect( dataProcessor.toData( fragment ) ).to.equal( '
' ); + } ); + } ); } ); diff --git a/packages/ckeditor5-engine/tests/view/domconverter/domconverter.js b/packages/ckeditor5-engine/tests/view/domconverter/domconverter.js index 7806c1948ae..c30f28cc18f 100644 --- a/packages/ckeditor5-engine/tests/view/domconverter/domconverter.js +++ b/packages/ckeditor5-engine/tests/view/domconverter/domconverter.js @@ -10,7 +10,7 @@ import ViewEditable from '../../../src/view/editableelement'; import ViewDocument from '../../../src/view/document'; import ViewUIElement from '../../../src/view/uielement'; import ViewContainerElement from '../../../src/view/containerelement'; -import { BR_FILLER, INLINE_FILLER, INLINE_FILLER_LENGTH, NBSP_FILLER } from '../../../src/view/filler'; +import { BR_FILLER, INLINE_FILLER, INLINE_FILLER_LENGTH, NBSP_FILLER, MARKED_NBSP_FILLER } from '../../../src/view/filler'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import global from '@ckeditor/ckeditor5-utils/src/dom/global'; import { StylesProcessor } from '../../../src/view/stylesmap'; @@ -295,88 +295,96 @@ describe( 'DomConverter', () => { } ); describe( 'isBlockFiller()', () => { - describe( 'mode "nbsp"', () => { - beforeEach( () => { - converter = new DomConverter( viewDocument, { blockFillerMode: 'nbsp' } ); - } ); + for ( const mode of [ 'nbsp', 'markedNbsp' ] ) { + describe( 'mode "' + mode + '"', () => { + beforeEach( () => { + converter = new DomConverter( viewDocument, { blockFillerMode: mode } ); + } ); - it( 'should return true if the node is an nbsp filler and is a single child of a block level element', () => { - const nbspFillerInstance = NBSP_FILLER( document ); // eslint-disable-line new-cap + it( 'should return true if the node is an nbsp filler and is a single child of a block level element', () => { + const nbspFillerInstance = NBSP_FILLER( document ); // eslint-disable-line new-cap - const context = document.createElement( 'div' ); - context.appendChild( nbspFillerInstance ); + const context = document.createElement( 'div' ); + context.appendChild( nbspFillerInstance ); - expect( converter.isBlockFiller( nbspFillerInstance ) ).to.be.true; - } ); + expect( converter.isBlockFiller( nbspFillerInstance ) ).to.be.true; + } ); - it( 'should return false if the node is an nbsp filler and is not a single child of a block level element', () => { - const nbspFillerInstance = NBSP_FILLER( document ); // eslint-disable-line new-cap + it( 'should return false if the node is an nbsp filler and is not a single child of a block level element', () => { + const nbspFillerInstance = NBSP_FILLER( document ); // eslint-disable-line new-cap - const context = document.createElement( 'div' ); - context.appendChild( nbspFillerInstance ); - context.appendChild( document.createTextNode( 'a' ) ); + const context = document.createElement( 'div' ); + context.appendChild( nbspFillerInstance ); + context.appendChild( document.createTextNode( 'a' ) ); - expect( converter.isBlockFiller( nbspFillerInstance ) ).to.be.false; - } ); + expect( converter.isBlockFiller( nbspFillerInstance ) ).to.be.false; + } ); - it( 'should return false if there are two nbsp fillers in a block element', () => { - const nbspFillerInstance = NBSP_FILLER( document ); // eslint-disable-line new-cap + it( 'should return false if there are two nbsp fillers in a block element', () => { + const nbspFillerInstance = NBSP_FILLER( document ); // eslint-disable-line new-cap - const context = document.createElement( 'div' ); - context.appendChild( nbspFillerInstance ); - context.appendChild( NBSP_FILLER( document ) ); // eslint-disable-line new-cap + const context = document.createElement( 'div' ); + context.appendChild( nbspFillerInstance ); + context.appendChild( NBSP_FILLER( document ) ); // eslint-disable-line new-cap - expect( converter.isBlockFiller( nbspFillerInstance ) ).to.be.false; - } ); + expect( converter.isBlockFiller( nbspFillerInstance ) ).to.be.false; + } ); - it( 'should return false filler is placed in a non-block element', () => { - const nbspFillerInstance = NBSP_FILLER( document ); // eslint-disable-line new-cap + it( 'should return false filler is placed in a non-block element', () => { + const nbspFillerInstance = NBSP_FILLER( document ); // eslint-disable-line new-cap - const context = document.createElement( 'span' ); - context.appendChild( nbspFillerInstance ); + const context = document.createElement( 'span' ); + context.appendChild( nbspFillerInstance ); - expect( converter.isBlockFiller( nbspFillerInstance ) ).to.be.false; - } ); + expect( converter.isBlockFiller( nbspFillerInstance ) ).to.be.false; + } ); - it( 'should return false if the node is an instance of the BR block filler', () => { - const brFillerInstance = BR_FILLER( document ); // eslint-disable-line new-cap + it( 'should return false if the node is an instance of the BR block filler', () => { + const brFillerInstance = BR_FILLER( document ); // eslint-disable-line new-cap - expect( converter.isBlockFiller( brFillerInstance ) ).to.be.false; - } ); + expect( converter.isBlockFiller( brFillerInstance ) ).to.be.false; + } ); - it( 'should return false for inline filler', () => { - expect( converter.isBlockFiller( document.createTextNode( INLINE_FILLER ) ) ).to.be.false; - } ); + it( 'should return false for inline filler', () => { + expect( converter.isBlockFiller( document.createTextNode( INLINE_FILLER ) ) ).to.be.false; + } ); - it( 'should return false for a normal