Skip to content

Commit 602d4e1

Browse files
authored
Add EmptyBlock plugin that prevents adding   in exported data. (#17756)
Feature (html-support): Add an experimental `EmptyBlock` plugin that prevents adding ` ` to output data. Other (engine): The whitespaces around a block filler (` `) are ignored while loading editor data.
1 parent 0206060 commit 602d4e1

File tree

17 files changed

+1501
-16
lines changed

17 files changed

+1501
-16
lines changed

packages/ckeditor5-engine/src/view/domconverter.ts

Lines changed: 87 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,11 @@ export default class DomConverter {
725725
// Whitespace cleaning.
726726
this._processDomInlineNodes( null, inlineNodes, options );
727727

728+
// This was a single block filler so just remove it.
729+
if ( this.blockFillerMode == 'br' && isViewBrFiller( node ) ) {
730+
return null;
731+
}
732+
728733
// Text not got trimmed to an empty string so there is no result node.
729734
if ( node.is( '$text' ) && node.data.length == 0 ) {
730735
return null;
@@ -770,7 +775,10 @@ export default class DomConverter {
770775
this._processDomInlineNodes( domElement, inlineNodes, options );
771776
}
772777

773-
yield viewChild;
778+
// Yield only if this is not a block filler.
779+
if ( !( this.blockFillerMode == 'br' && isViewBrFiller( viewChild ) ) ) {
780+
yield viewChild;
781+
}
774782

775783
// Trigger children handling.
776784
generator.next();
@@ -1184,12 +1192,9 @@ export default class DomConverter {
11841192
return domNode.isEqualNode( BR_FILLER_REF );
11851193
}
11861194

1187-
// Special case for <p><br></p> in which <br> should be treated as filler even when we are not in the 'br' mode. See ckeditor5#5564.
1188-
if (
1189-
( domNode as DomElement ).tagName === 'BR' &&
1190-
hasBlockParent( domNode, this.blockElements ) &&
1191-
( domNode as DomElement ).parentNode!.childNodes.length === 1
1192-
) {
1195+
// Special case for <p><br></p> in which <br> should be treated as filler even when we are not in the 'br' mode.
1196+
// See https://github.com/ckeditor/ckeditor5/issues/5564.
1197+
if ( isOnlyBrInBlock( domNode as DomElement, this.blockElements ) ) {
11931198
return true;
11941199
}
11951200

@@ -1373,7 +1378,9 @@ export default class DomConverter {
13731378
},
13741379
inlineNodes: Array<ViewNode>
13751380
): IterableIterator<ViewNode | ViewDocumentFragment | null> {
1376-
if ( this.isBlockFiller( domNode ) ) {
1381+
// Special case for <p><br></p> in which <br> should be treated as filler even when we are not in the 'br' mode.
1382+
// See https://github.com/ckeditor/ckeditor5/issues/5564.
1383+
if ( this.blockFillerMode != 'br' && isOnlyBrInBlock( domNode as DomElement, this.blockElements ) ) {
13771384
return null;
13781385
}
13791386

@@ -1557,6 +1564,23 @@ export default class DomConverter {
15571564
// the inline filler is removed only after the data is initially processed (by the `.replace` above). See ckeditor5#692.
15581565
data = getDataWithoutFiller( data );
15591566

1567+
// Block filler handling.
1568+
if ( this.blockFillerMode != 'br' && node.parent ) {
1569+
if ( isViewMarkedNbspFiller( node.parent, data ) ) {
1570+
data = '';
1571+
1572+
// Mark block element as it has a block filler and remove the `<span data-cke-filler="true">` element.
1573+
if ( node.parent.parent ) {
1574+
node.parent.parent._setCustomProperty( '$hasBlockFiller', true );
1575+
node.parent._remove();
1576+
}
1577+
}
1578+
else if ( isViewNbspFiller( node.parent, data, this.blockElements ) ) {
1579+
data = '';
1580+
node.parent._setCustomProperty( '$hasBlockFiller', true );
1581+
}
1582+
}
1583+
15601584
// At this point we should have removed all whitespaces from DOM text data.
15611585
//
15621586
// Now, We will reverse the process that happens in `_processDataFromViewText`.
@@ -1856,7 +1880,7 @@ function forEachDomElementAncestor( element: DomElement, callback: ( node: DomEl
18561880
}
18571881

18581882
/**
1859-
* Checks if given node is a nbsp block filler.
1883+
* Checks if given DOM node is a nbsp block filler.
18601884
*
18611885
* A &nbsp; is a block filler only if it is a single child of a block element.
18621886
*
@@ -1879,6 +1903,60 @@ function hasBlockParent( domNode: DomNode, blockElements: ReadonlyArray<string>
18791903
return !!parent && !!( parent as DomElement ).tagName && blockElements.includes( ( parent as DomElement ).tagName.toLowerCase() );
18801904
}
18811905

1906+
/**
1907+
* Checks if given view node is a nbsp block filler.
1908+
*
1909+
* A &nbsp; is a block filler only if it is a single child of a block element.
1910+
*/
1911+
function isViewNbspFiller( parent: ViewNode | ViewDocumentFragment, data: string, blockElements: Array<string> ): boolean {
1912+
return (
1913+
data == '\u00A0' &&
1914+
parent &&
1915+
parent.is( 'element' ) &&
1916+
parent.childCount == 1 &&
1917+
blockElements.includes( parent.name )
1918+
);
1919+
}
1920+
1921+
/**
1922+
* Checks if given view node is a marked-nbsp block filler.
1923+
*
1924+
* A &nbsp; is a block filler only if it is wrapped in `<span data-cke-filler="true">` element.
1925+
*/
1926+
function isViewMarkedNbspFiller( parent: ViewNode | ViewDocumentFragment, data: string ): boolean {
1927+
return (
1928+
data == '\u00A0' &&
1929+
parent &&
1930+
parent.is( 'element', 'span' ) &&
1931+
parent.childCount == 1 &&
1932+
parent.hasAttribute( 'data-cke-filler' )
1933+
);
1934+
}
1935+
1936+
/**
1937+
* Checks if given view node is a br block filler.
1938+
*
1939+
* A <br> is a block filler only if it has data-cke-filler attribute set.
1940+
*/
1941+
function isViewBrFiller( node: ViewNode ): boolean {
1942+
return (
1943+
node.is( 'element', 'br' ) &&
1944+
node.hasAttribute( 'data-cke-filler' )
1945+
);
1946+
}
1947+
1948+
/**
1949+
* Special case for `<p><br></p>` in which `<br>` should be treated as filler even when we are not in the 'br' mode.
1950+
*/
1951+
function isOnlyBrInBlock( domNode: DomElement, blockElements: Array<string> ): boolean {
1952+
// See https://github.com/ckeditor/ckeditor5/issues/5564.
1953+
return (
1954+
domNode.tagName === 'BR' &&
1955+
hasBlockParent( domNode, blockElements ) &&
1956+
domNode.parentNode!.childNodes.length === 1
1957+
);
1958+
}
1959+
18821960
/**
18831961
* Log to console the information about element that was replaced.
18841962
* Check UNSAFE_ELEMENTS for all recognized unsafe elements.

packages/ckeditor5-engine/tests/view/domconverter/dom-to-view.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,16 @@ describe( 'DomConverter', () => {
171171
expect( converter.domToView( domFiller ) ).to.be.null;
172172
} );
173173

174+
it( 'should ignore a block filler inside a paragraph', () => {
175+
// eslint-disable-next-line new-cap
176+
const domFiller = BR_FILLER( document );
177+
const domP = createElement( document, 'p', undefined, [ domFiller ] );
178+
179+
const viewP = converter.domToView( domP );
180+
expect( viewP.is( 'element', 'p' ) ).to.be.true;
181+
expect( viewP.childCount ).to.equal( 0 );
182+
} );
183+
174184
it( 'should return null for empty text node', () => {
175185
const textNode = document.createTextNode( '' );
176186

packages/ckeditor5-engine/tests/view/domconverter/whitespace-handling-integration.js

Lines changed: 160 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
44
*/
55

6+
/* globals document */
7+
68
import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor.js';
79
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph.js';
810
import ImageInlineEditing from '@ckeditor/ckeditor5-image/src/image/imageinlineediting.js';
911
import ShiftEnter from '@ckeditor/ckeditor5-enter/src/shiftenter.js';
12+
import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement.js';
1013

1114
import { getData } from '../../../src/dev-utils/model.js';
15+
import { getFillerOffset } from '../../../src/index.js';
1216

1317
// NOTE:
1418
// dev utils' setData() loses white spaces so don't use it for tests here!!!
@@ -193,13 +197,60 @@ describe( 'DomConverter – whitespace handling – integration', () => {
193197
expect( editor.getData() ).to.equal( '<p>&nbsp;foo&nbsp;</p>' );
194198
} );
195199

196-
it( 'single nbsp inside blocks are ignored', () => {
200+
it( 'single nbsp inside blocks is ignored (NBSP block filler)', () => {
197201
editor.setData( '<p>&nbsp;</p>' );
198202

199203
expect( getData( editor.model, { withoutSelection: true } ) )
200204
.to.equal( '<paragraph></paragraph>' );
201205

202206
expect( editor.getData() ).to.equal( '' ); // trimmed
207+
expect( editor.getData( { trim: false } ) ).to.equal( '<p>&nbsp;</p>' );
208+
} );
209+
210+
it( 'nbsp with spaces inside blocks is ignored (NBSP block filler)', () => {
211+
editor.setData( '<p>\n &nbsp;\n </p>' );
212+
213+
expect( getData( editor.model, { withoutSelection: true } ) )
214+
.to.equal( '<paragraph></paragraph>' );
215+
216+
expect( editor.getData() ).to.equal( '' ); // trimmed
217+
expect( editor.getData( { trim: false } ) ).to.equal( '<p>&nbsp;</p>' );
218+
} );
219+
220+
it( 'single nbsp inside blocks is ignored (marked NBSP block filler)', () => {
221+
editor.data.processor.useFillerType( 'marked' );
222+
223+
editor.conversion.for( 'upcast' ).add( dispatcher => {
224+
dispatcher.on( 'element', ( evt, data ) => {
225+
expect( data.viewItem.name ).to.not.equal( 'span' );
226+
} );
227+
} );
228+
229+
editor.setData( '<p><span data-cke-filler="true">&nbsp;</span></p>' );
230+
231+
expect( getData( editor.model, { withoutSelection: true } ) )
232+
.to.equal( '<paragraph></paragraph>' );
233+
234+
expect( editor.getData() ).to.equal( '' ); // trimmed
235+
expect( editor.getData( { trim: false } ) ).to.equal( '<p><span data-cke-filler="true">&nbsp;</span></p>' );
236+
} );
237+
238+
it( 'nbsp with spaces inside blocks are ignored (marked NBSP block filler)', () => {
239+
editor.data.processor.useFillerType( 'marked' );
240+
241+
editor.conversion.for( 'upcast' ).add( dispatcher => {
242+
dispatcher.on( 'element', ( evt, data ) => {
243+
expect( data.viewItem.name ).to.not.equal( 'span' );
244+
} );
245+
} );
246+
247+
editor.setData( '<p>\n <span data-cke-filler="true">&nbsp;</span>\n </p>' );
248+
249+
expect( getData( editor.model, { withoutSelection: true } ) )
250+
.to.equal( '<paragraph></paragraph>' );
251+
252+
expect( editor.getData() ).to.equal( '' ); // trimmed
253+
expect( editor.getData( { trim: false } ) ).to.equal( '<p><span data-cke-filler="true">&nbsp;</span></p>' );
203254
} );
204255

205256
it( 'all whitespaces together are ignored', () => {
@@ -878,6 +929,114 @@ describe( 'DomConverter – whitespace handling – integration', () => {
878929
'</ul>'
879930
);
880931
} );
932+
933+
describe( 'text nodes parse and stringify', () => {
934+
function testTexts( inputTexts, processedText, outputText ) {
935+
if ( typeof inputTexts == 'string' ) {
936+
inputTexts = [ inputTexts ];
937+
}
938+
939+
outputText = outputText !== undefined ? outputText : inputTexts.join( '' );
940+
941+
it( `spaces in a text node: "${ inputTexts.join( '|' ) }" -> "${ processedText }" -> "${ outputText }"`, () => {
942+
const domElement = createElement( document, 'p', {}, [] );
943+
944+
for ( const text of inputTexts ) {
945+
domElement.appendChild( document.createTextNode( text.replace( /_/g, '\u00A0' ) ) );
946+
}
947+
948+
const viewElement = editor.data.processor.domConverter.domToView( domElement );
949+
let viewData = '';
950+
951+
viewElement.getFillerOffset = getFillerOffset;
952+
953+
for ( const child of viewElement.getChildren() ) {
954+
viewData += child.data.replace( /\u00A0/g, '_' );
955+
}
956+
957+
expect( viewData, 'processed' ).to.equal( processedText );
958+
959+
const outputDomElement = editor.data.processor.domConverter.viewToDom( viewElement );
960+
961+
expect( outputDomElement.innerHTML.replace( /&nbsp;/g, '_' ), 'output' ).to.equal( outputText );
962+
} );
963+
}
964+
965+
// Block filler.
966+
testTexts( '_', '', '_' );
967+
testTexts( ' _ ', '', '_' );
968+
testTexts( ' _ ', '', '_' );
969+
970+
// At the beginning.
971+
testTexts( '_x', ' x' );
972+
testTexts( '_ x', ' x' );
973+
testTexts( '_ _x', ' x' );
974+
testTexts( '_ _ x', ' x' );
975+
976+
// At the end.
977+
testTexts( 'x_', 'x ' );
978+
testTexts( 'x _', 'x ' );
979+
testTexts( 'x __', 'x ' );
980+
testTexts( 'x _ _', 'x ' );
981+
982+
// In the middle.
983+
testTexts( 'x x', 'x x' );
984+
testTexts( 'x _x', 'x x' );
985+
testTexts( 'x _ x', 'x x' );
986+
testTexts( 'x _ _x', 'x x' );
987+
988+
// Complex.
989+
testTexts( '_x_', ' x ' );
990+
testTexts( '_ x _x _', ' x x ' );
991+
testTexts( '_ _x x _', ' x x ' );
992+
testTexts( '_ _x x __', ' x x ' );
993+
testTexts( '_ _x _ _x_', ' x x ' );
994+
995+
// With hard &nbsp;
996+
testTexts( '_x', ' x' );
997+
testTexts( '__x', ' _x' );
998+
testTexts( '___x', ' __x' );
999+
testTexts( '__ x', ' _ x' );
1000+
1001+
testTexts( 'x_', 'x ' );
1002+
testTexts( 'x__', 'x_ ' );
1003+
testTexts( 'x___', 'x__ ' );
1004+
1005+
testTexts( 'x_x', 'x_x' );
1006+
testTexts( 'x___x', 'x___x' );
1007+
testTexts( 'x____x', 'x____x' );
1008+
testTexts( 'x__ x', 'x__ x' );
1009+
testTexts( 'x___ x', 'x___ x' );
1010+
testTexts( 'x_ _x', 'x_ x' );
1011+
testTexts( 'x __x', 'x _x' );
1012+
testTexts( 'x _ x', 'x x' );
1013+
testTexts( 'x __ _x', 'x _ x' );
1014+
1015+
// Two text nodes.
1016+
testTexts( [ 'x', 'y' ], 'xy' );
1017+
testTexts( [ 'x ', 'y' ], 'x y' );
1018+
testTexts( [ 'x _', 'y' ], 'x y' );
1019+
testTexts( [ 'x __', 'y' ], 'x y' );
1020+
testTexts( [ 'x _ _', 'y' ], 'x y', 'x _ _y' );
1021+
1022+
testTexts( [ 'x', ' y' ], 'x y' );
1023+
testTexts( [ 'x_', ' y' ], 'x y' );
1024+
testTexts( [ 'x _', ' y' ], 'x y' );
1025+
testTexts( [ 'x __', ' y' ], 'x y' );
1026+
testTexts( [ 'x _ _', ' y' ], 'x y' );
1027+
1028+
testTexts( [ 'x', ' _y' ], 'x y' );
1029+
testTexts( [ 'x_', ' _y' ], 'x y' );
1030+
testTexts( [ 'x _', ' _y' ], 'x y' );
1031+
testTexts( [ 'x __', ' _y' ], 'x y' );
1032+
testTexts( [ 'x _ _', ' _y' ], 'x y' );
1033+
1034+
// Some tests with hard &nbsp;
1035+
testTexts( [ 'x', '_y' ], 'x_y' );
1036+
testTexts( [ 'x_', 'y' ], 'x_y' );
1037+
testTexts( [ 'x__', ' y' ], 'x_ y' );
1038+
testTexts( [ 'x_ _', ' y' ], 'x_ y' );
1039+
} );
8811040
} );
8821041

8831042
// https://github.com/ckeditor/ckeditor5/issues/1024

packages/ckeditor5-html-support/src/augmentation.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import type {
1919
StyleElementSupport,
2020
TableElementSupport,
2121
HtmlComment,
22-
FullPage
22+
FullPage,
23+
EmptyBlock
2324
} from './index.js';
2425

2526
declare module '@ckeditor/ckeditor5-core' {
@@ -50,5 +51,6 @@ declare module '@ckeditor/ckeditor5-core' {
5051
[ TableElementSupport.pluginName ]: TableElementSupport;
5152
[ HtmlComment.pluginName ]: HtmlComment;
5253
[ FullPage.pluginName ]: FullPage;
54+
[ EmptyBlock.pluginName ]: EmptyBlock;
5355
}
5456
}

0 commit comments

Comments
 (0)