Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Commit 9ca61d5

Browse files
authored
Merge pull request #1083 from ckeditor/t/ckeditor5-typing/101
Fix: Multiple spaces in an empty paragraph are now allowed. Closes ckeditor/ckeditor5-typing#101.
2 parents ed1b7e7 + 9515717 commit 9ca61d5

File tree

2 files changed

+141
-103
lines changed

2 files changed

+141
-103
lines changed

src/view/domconverter.js

Lines changed: 61 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export default class DomConverter {
6565
*
6666
* @member {Array.<String>} module:engine/view/domconverter~DomConverter#preElements
6767
*/
68-
this.preElements = [ 'pre' ];
68+
this.preElements = [ 'pre', 'code' ];
6969

7070
/**
7171
* Tag names of DOM `Element`s which are considered block elements.
@@ -177,7 +177,7 @@ export default class DomConverter {
177177
* @param {Document} domDocument Document which will be used to create DOM nodes.
178178
* @param {Object} [options] Conversion options.
179179
* @param {Boolean} [options.bind=false] Determines whether new elements will be bound.
180-
* @param {Boolean} [options.withChildren=true] If true node's and document fragment's children will be converted too.
180+
* @param {Boolean} [options.withChildren=true] If `true`, node's and document fragment's children will be converted too.
181181
* @returns {Node|DocumentFragment} Converted node or DocumentFragment.
182182
*/
183183
viewToDom( viewNode, domDocument, options = {} ) {
@@ -885,127 +885,69 @@ export default class DomConverter {
885885
}
886886

887887
/**
888-
* Takes text data from given {@link module:engine/view/text~Text#data} and processes it so it is correctly displayed in DOM.
888+
* Takes text data from a given {@link module:engine/view/text~Text#data} and processes it so
889+
* it is correctly displayed in the DOM.
889890
*
890891
* Following changes are done:
891892
*
892-
* * multiple spaces are replaced to a chain of spaces and `&nbsp;`,
893-
* * space at the beginning of the text node is changed to `&nbsp;` if it is a first text node in it's container
894-
* element or if previous text node ends by space character,
895-
* * space at the end of the text node is changed to `&nbsp;` if it is a last text node in it's container.
893+
* * a space at the beginning is changed to `&nbsp;` if this is the first text node in its container
894+
* element or if a previous text node ends with a space character,
895+
* * space at the end of the text node is changed to `&nbsp;` if this is the last text node in its container,
896+
* * remaining spaces are replaced to a chain of spaces and `&nbsp;` (e.g. `'x x'` becomes `'x &nbsp; x'`).
897+
*
898+
* Content of {@link #preElements} is not processed.
896899
*
897900
* @private
898901
* @param {module:engine/view/text~Text} node View text node to process.
899902
* @returns {String} Processed text data.
900903
*/
901904
_processDataFromViewText( node ) {
902-
const data = node.data;
905+
let data = node.data;
903906

904907
// If any of node ancestors has a name which is in `preElements` array, then currently processed
905908
// view text node is (will be) in preformatted element. We should not change whitespaces then.
906909
if ( node.getAncestors().some( parent => this.preElements.includes( parent.name ) ) ) {
907910
return data;
908911
}
909912

910-
const prevNode = this._getTouchingViewTextNode( node, false );
911-
const nextNode = this._getTouchingViewTextNode( node, true );
912-
913-
// Second part of text data, from the space after the last non-space character to the end.
914-
// We separate `textEnd` and `textStart` because `textEnd` needs some special handling.
915-
let textEnd = data.match( / *$/ )[ 0 ];
916-
// First part of data, between first and last part of data.
917-
let textStart = data.substr( 0, data.length - textEnd.length );
918-
919-
// If previous text node does not exist or it ends by space character, replace space character at the beginning of text.
920-
// ` x` -> `_x`
921-
// ` x` -> `_ x`
922-
// ` x` -> `_ x`
923-
if ( !prevNode || prevNode.data.charAt( prevNode.data.length - 1 ) == ' ' ) {
924-
textStart = textStart.replace( /^ /, '\u00A0' );
913+
// 1. Replace the first space with a nbsp if the previous node ends with a space or there is no previous node
914+
// (container element boundary).
915+
if ( data.charAt( 0 ) == ' ' ) {
916+
const prevNode = this._getTouchingViewTextNode( node, false );
917+
const prevEndsWithSpace = prevNode && this._nodeEndsWithSpace( prevNode );
918+
919+
if ( prevEndsWithSpace || !prevNode ) {
920+
data = '\u00A0' + data.substr( 1 );
921+
}
925922
}
926923

927-
// Multiple consecutive spaces. Change them to ` &nbsp;` pairs.
928-
// `_x x` -> `_x _x`
929-
// `_ x x` -> `_ x _x`
930-
// `_ x x` -> `_ _x _x`
931-
// `_ x x` -> `_ _x _ x`
932-
// `_ x x` -> `_ _x _ _x`
933-
// `_ x x` -> `_ _ x _ _x`
934-
textStart = textStart.replace( / {2}/g, ' \u00A0' );
935-
936-
// Process `textEnd` only if there is anything to process.
937-
if ( textEnd.length > 0 ) {
938-
// (1) We need special treatment for the last part of text node, it has to end on `&nbsp;`, not space:
939-
// `x ` -> `x_`
940-
// `x ` -> `x _`
941-
// `x ` -> `x_ _`
942-
// `x ` -> `x _ _`
943-
// (2) Different case when there is a node after:
944-
// `x <b>b</b>` -> `x <b>b</b>`
945-
// `x <b>b</b>` -> `x _<b>b</b>`
946-
// `x <b>b</b>` -> `x _ <b>b</b>`
947-
// `x <b>b</b>` -> `x _ _<b>b</b>`
948-
// (3) But different, when that node starts by &nbsp; (or space that will be converted to &nbsp;):
949-
// `x <b>_b</b>` -> `x <b>_b</b>`
950-
// `x <b>_b</b>` -> `x_ <b>_b</b>`
951-
// `x <b>_b</b>` -> `x _ <b>_b</b>`
952-
// `x <b>_b</b>` -> `x_ _ <b>_b</b>`
953-
// Let's assume that starting from space is normal behavior, because starting from &nbsp; is a less frequent case.
954-
let textEndStartsFromNbsp = false;
924+
// 2. Replace the last space with a nbsp if this is the last text node (container element boundary).
925+
if ( data.charAt( data.length - 1 ) == ' ' ) {
926+
const nextNode = this._getTouchingViewTextNode( node, true );
955927

956928
if ( !nextNode ) {
957-
// (1)
958-
if ( textEnd.length % 2 ) {
959-
textEndStartsFromNbsp = true;
960-
}
961-
} else if ( nextNode.data.charAt( 0 ) == ' ' || nextNode.data.charAt( 0 ) == '\u00A0' ) {
962-
// (3)
963-
if ( textEnd.length % 2 === 0 ) {
964-
textEndStartsFromNbsp = true;
965-
}
929+
data = data.substr( 0, data.length - 1 ) + '\u00A0';
966930
}
967-
968-
if ( textEndStartsFromNbsp ) {
969-
textEnd = '\u00A0' + textEnd.substr( 0, textEnd.length - 1 );
970-
}
971-
972-
textEnd = textEnd.replace( / {2}/g, ' \u00A0' );
973931
}
974932

975-
return textStart + textEnd;
933+
return data.replace( / {2}/g, ' \u00A0' );
976934
}
977935

978936
/**
979-
* Helper function. For given {@link module:engine/view/text~Text view text node}, it finds previous or next sibling that is contained
980-
* in the same block element. If there is no such sibling, `null` is returned.
937+
* Checks whether given node ends with a space character after changing appropriate space characters to `&nbsp;`s.
981938
*
982939
* @private
983-
* @param {module:engine/view/text~Text} node
984-
* @param {Boolean} getNext
985-
* @returns {module:engine/view/text~Text}
940+
* @param {module:engine/view/text~Text} node Node to check.
941+
* @returns {Boolean} `true` if given `node` ends with space, `false` otherwise.
986942
*/
987-
_getTouchingViewTextNode( node, getNext ) {
988-
if ( !node.parent ) {
989-
return null;
943+
_nodeEndsWithSpace( node ) {
944+
if ( node.getAncestors().some( parent => this.preElements.includes( parent.name ) ) ) {
945+
return false;
990946
}
991947

992-
const treeWalker = new ViewTreeWalker( {
993-
startPosition: getNext ? ViewPosition.createAfter( node ) : ViewPosition.createBefore( node ),
994-
direction: getNext ? 'forward' : 'backward'
995-
} );
948+
const data = this._processDataFromViewText( node );
996949

997-
for ( const value of treeWalker ) {
998-
if ( value.item.is( 'containerElement' ) ) {
999-
// ViewContainerElement is found on a way to next ViewText node, so given `node` was first/last
1000-
// text node in it's container element.
1001-
return null;
1002-
} else if ( value.item.is( 'text' ) ) {
1003-
// Found a text node in the same container element.
1004-
return value.item;
1005-
}
1006-
}
1007-
1008-
return null;
950+
return data.charAt( data.length - 1 ) == ' ';
1009951
}
1010952

1011953
/**
@@ -1075,6 +1017,34 @@ export default class DomConverter {
10751017
return data;
10761018
}
10771019

1020+
/**
1021+
* Helper function. For given {@link module:engine/view/text~Text view text node}, it finds previous or next sibling
1022+
* that is contained in the same container element. If there is no such sibling, `null` is returned.
1023+
*
1024+
* @param {module:engine/view/text~Text} node Reference node.
1025+
* @param {Boolean} getNext
1026+
* @returns {module:engine/view/text~Text|null} Touching text node or `null` if there is no next or previous touching text node.
1027+
*/
1028+
_getTouchingViewTextNode( node, getNext ) {
1029+
const treeWalker = new ViewTreeWalker( {
1030+
startPosition: getNext ? ViewPosition.createAfter( node ) : ViewPosition.createBefore( node ),
1031+
direction: getNext ? 'forward' : 'backward'
1032+
} );
1033+
1034+
for ( const value of treeWalker ) {
1035+
if ( value.item.is( 'containerElement' ) ) {
1036+
// ViewContainerElement is found on a way to next ViewText node, so given `node` was first/last
1037+
// text node in its container element.
1038+
return null;
1039+
} else if ( value.item.is( 'text' ) ) {
1040+
// Found a text node in the same container element.
1041+
return value.item;
1042+
}
1043+
}
1044+
1045+
return null;
1046+
}
1047+
10781048
/**
10791049
* Helper function. For given `Text` node, it finds previous or next sibling that is contained in the same block element.
10801050
* If there is no such sibling, `null` is returned.

tests/view/domconverter/view-to-dom.js

Lines changed: 80 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ describe( 'DomConverter', () => {
269269
// At the end.
270270
test( 'x ', 'x_' );
271271
test( 'x ', 'x _' );
272-
test( 'x ', 'x_ _' );
272+
test( 'x ', 'x __' );
273273
test( 'x ', 'x _ _' );
274274

275275
// In the middle.
@@ -282,11 +282,19 @@ describe( 'DomConverter', () => {
282282
test( ' x ', '_x_' );
283283
test( ' x x ', '_ x _x _' );
284284
test( ' x x ', '_ _x x _' );
285-
test( ' x x ', '_ _x x_ _' );
285+
test( ' x x ', '_ _x x __' );
286286
test( ' x x ', '_ _x _ _x_' );
287+
288+
// Only spaces.
287289
test( ' ', '_' );
290+
test( ' ', '__' );
291+
test( ' ', '_ _' );
292+
test( ' ', '_ __' );
293+
test( ' ', '_ _ _' );
294+
test( ' ', '_ _ __' );
288295

289296
// With hard &nbsp;
297+
// It should be treated like a normal sign.
290298
test( '_x', '_x' );
291299
test( ' _x', '__x' );
292300
test( ' _x', '_ _x' );
@@ -323,33 +331,93 @@ describe( 'DomConverter', () => {
323331

324332
test( [ 'x', ' y' ], 'x y' );
325333
test( [ 'x ', ' y' ], 'x _y' );
326-
test( [ 'x ', ' y' ], 'x_ _y' );
334+
test( [ 'x ', ' y' ], 'x _ y' );
327335
test( [ 'x ', ' y' ], 'x _ _y' );
328-
test( [ 'x ', ' y' ], 'x_ _ _y' );
336+
test( [ 'x ', ' y' ], 'x _ _ y' );
329337

330338
test( [ 'x', '_y' ], 'x_y' );
331339
test( [ 'x ', '_y' ], 'x _y' );
332-
test( [ 'x ', '_y' ], 'x_ _y' );
340+
test( [ 'x ', '_y' ], 'x __y' );
333341
test( [ 'x ', '_y' ], 'x _ _y' );
334-
test( [ 'x ', '_y' ], 'x_ _ _y' );
342+
test( [ 'x ', '_y' ], 'x _ __y' );
335343

336344
test( [ 'x', ' y' ], 'x _y' );
337345
test( [ 'x ', ' y' ], 'x _ y' );
338-
test( [ 'x ', ' y' ], 'x_ _ y' );
346+
test( [ 'x ', ' y' ], 'x _ _y' );
339347
test( [ 'x ', ' y' ], 'x _ _ y' );
340-
test( [ 'x ', ' y' ], 'x_ _ _ y' );
348+
test( [ 'x ', ' y' ], 'x _ _ _y' );
341349

342350
test( [ 'x', ' y' ], 'x _ y' );
343351
test( [ 'x ', ' y' ], 'x _ _y' );
344-
test( [ 'x ', ' y' ], 'x_ _ _y' );
352+
test( [ 'x ', ' y' ], 'x _ _ y' );
345353
test( [ 'x ', ' y' ], 'x _ _ _y' );
346-
test( [ 'x ', ' y' ], 'x_ _ _ _y' );
354+
test( [ 'x ', ' y' ], 'x _ _ _ y' );
355+
356+
// "Non-empty" + "empty" text nodes.
357+
test( [ 'x', ' ' ], 'x_' );
358+
test( [ 'x', ' ' ], 'x _' );
359+
test( [ 'x', ' ' ], 'x __' );
360+
test( [ 'x ', ' ' ], 'x _' );
361+
test( [ 'x ', ' ' ], 'x __' );
362+
test( [ 'x ', ' ' ], 'x _ _' );
363+
test( [ 'x ', ' ' ], 'x __' );
364+
test( [ 'x ', ' ' ], 'x _ _' );
365+
test( [ 'x ', ' ' ], 'x _ __' );
366+
test( [ 'x ', ' ' ], 'x _ _' );
367+
test( [ 'x ', ' ' ], 'x _ __' );
368+
test( [ 'x ', ' ' ], 'x _ _ _' );
369+
370+
test( [ ' ', 'x' ], '_x' );
371+
test( [ ' ', 'x' ], '_ x' );
372+
test( [ ' ', 'x' ], '_ _x' );
373+
test( [ ' ', ' x' ], '_ x' );
374+
test( [ ' ', ' x' ], '_ _x' );
375+
test( [ ' ', ' x' ], '_ _ x' );
376+
test( [ ' ', ' x' ], '_ _x' );
377+
test( [ ' ', ' x' ], '_ _ x' );
378+
test( [ ' ', ' x' ], '_ _ _x' );
379+
test( [ ' ', ' x' ], '_ _ x' );
380+
test( [ ' ', ' x' ], '_ _ _x' );
381+
test( [ ' ', ' x' ], '_ _ _ x' );
382+
383+
test( [ 'x', ' ', 'x' ], 'x x' );
384+
test( [ 'x', ' ', ' x' ], 'x _x' );
385+
test( [ 'x', ' ', ' x' ], 'x _ x' );
386+
test( [ 'x', ' ', ' x' ], 'x _ _ x' );
387+
test( [ 'x ', ' ', ' x' ], 'x _ x' );
388+
test( [ 'x ', ' ', ' x' ], 'x _ _x' );
389+
test( [ 'x ', ' ', ' x' ], 'x _ _ _x' );
390+
test( [ 'x ', ' ', ' x' ], 'x _ _x' );
391+
test( [ 'x ', ' ', ' x' ], 'x _ _ x' );
392+
test( [ 'x ', ' ', ' x' ], 'x _ _ _ x' );
393+
test( [ 'x ', ' ', ' x' ], 'x _ _ x' );
394+
test( [ 'x ', ' ', ' x' ], 'x _ _ _x' );
395+
test( [ 'x ', ' ', ' x' ], 'x _ _ _ _x' );
396+
397+
// "Empty" + "empty" text nodes.
398+
test( [ ' ', ' ' ], '__' );
399+
test( [ ' ', ' ' ], '_ _' );
400+
test( [ ' ', ' ' ], '_ __' );
401+
test( [ ' ', ' ' ], '_ _' );
402+
test( [ ' ', ' ' ], '_ __' );
403+
test( [ ' ', ' ' ], '_ __' );
404+
test( [ ' ', ' ' ], '_ _ _' );
405+
test( [ ' ', ' ' ], '_ _ _' );
406+
test( [ ' ', ' ' ], '_ _ __' );
347407

348408
it( 'not in preformatted blocks', () => {
349-
const viewDiv = new ViewContainerElement( 'pre', null, new ViewText( ' foo ' ) );
409+
const viewDiv = new ViewContainerElement( 'pre', null, [ new ViewText( ' foo ' ), new ViewText( ' bar ' ) ] );
410+
const domDiv = converter.viewToDom( viewDiv, document );
411+
412+
expect( domDiv.innerHTML ).to.equal( ' foo bar ' );
413+
} );
414+
415+
it( 'text node before in a preformatted node', () => {
416+
const viewCode = new ViewAttributeElement( 'code', null, new ViewText( 'foo ' ) );
417+
const viewDiv = new ViewContainerElement( 'div', null, [ viewCode, new ViewText( ' bar' ) ] );
350418
const domDiv = converter.viewToDom( viewDiv, document );
351419

352-
expect( domDiv.innerHTML ).to.equal( ' foo ' );
420+
expect( domDiv.innerHTML ).to.equal( '<code>foo </code> bar' );
353421
} );
354422
} );
355423
} );

0 commit comments

Comments
 (0)