diff --git a/src/view/domconverter.js b/src/view/domconverter.js index 95058b7c7..104b32a85 100644 --- a/src/view/domconverter.js +++ b/src/view/domconverter.js @@ -23,6 +23,7 @@ import indexOf from '@ckeditor/ckeditor5-utils/src/dom/indexof'; import getAncestors from '@ckeditor/ckeditor5-utils/src/dom/getancestors'; import getCommonAncestor from '@ckeditor/ckeditor5-utils/src/dom/getcommonancestor'; import isText from '@ckeditor/ckeditor5-utils/src/dom/istext'; +import isElement from '@ckeditor/ckeditor5-utils/src/lib/lodash/isElement'; /** * DomConverter is a set of tools to do transformations between DOM nodes and view nodes. It also handles @@ -948,10 +949,11 @@ export default class DomConverter { * Takes text data from native `Text` node and processes it to a correct {@link module:engine/view/text~Text view text node} data. * * Following changes are done: + * * * multiple whitespaces are replaced to a single space, - * * space at the beginning of the text node is removed, if it is a first text node in it's container - * element or if previous text node ends by space character, - * * space at the end of the text node is removed, if it is a last text node in it's container. + * * space at the beginning of a text node is removed if it is the first text node in its container + * element or if the previous text node ends with a space character, + * * space at the end of the text node is removed, if it is the last text node in its container. * * @param {Node} node DOM text node to process. * @returns {String} Processed data. @@ -970,17 +972,20 @@ export default class DomConverter { // We're replacing 1+ (and not 2+) to also normalize singular \n\t\r characters (#822). data = data.replace( /[ \n\t\r]{1,}/g, ' ' ); - const prevNode = this._getTouchingDomTextNode( node, false ); - const nextNode = this._getTouchingDomTextNode( node, true ); + const prevNode = this._getTouchingInlineDomNode( node, false ); + const nextNode = this._getTouchingInlineDomNode( node, true ); + + const shouldLeftTrim = this._checkShouldLeftTrimDomText( prevNode ); + const shouldRightTrim = this._checkShouldRightTrimDomText( node, nextNode ); - // If previous dom text node does not exist or it ends by whitespace character, remove space character from the beginning + // If the previous dom text node does not exist or it ends by whitespace character, remove space character from the beginning // of this text node. Such space character is treated as a whitespace. - if ( !prevNode || /[^\S\u00A0]/.test( prevNode.data.charAt( prevNode.data.length - 1 ) ) ) { + if ( shouldLeftTrim ) { data = data.replace( /^ /, '' ); } - // If next text node does not exist remove space character from the end of this text node. - if ( !nextNode && !startsWithFiller( node ) ) { + // If the next text node does not exist remove space character from the end of this text node. + if ( shouldRightTrim ) { data = data.replace( / $/, '' ); } @@ -1001,15 +1006,15 @@ export default class DomConverter { // Then, change   character that is at the beginning of the text node to space character. // As above, that   was created for rendering reasons but it's real meaning is just a space character. // We do that replacement only if this is the first node or the previous node ends on whitespace character. - if ( !prevNode || /[^\S\u00A0]/.test( prevNode.data.charAt( prevNode.data.length - 1 ) ) ) { + if ( shouldLeftTrim ) { data = data.replace( /^\u00A0/, ' ' ); } // Since input text data could be: `x_ _`, we would not replace the first   after `x` character. // We have to fix it. Since we already change all `  `, we will have something like this at the end of text data: // `x_ _ _` -> `x_ `. Find   at the end of string (can be followed only by spaces). - // We do that replacement only if this is the last node or the next node starts by  . - if ( !nextNode || nextNode.data.charAt( 0 ) == '\u00A0' ) { + // We do that replacement only if this is the last node or the next node starts with   or is a
. + if ( isText( nextNode ) ? nextNode.data.charAt( 0 ) == '\u00A0' : true ) { data = data.replace( /\u00A0( *)$/, ' $1' ); } @@ -1018,6 +1023,39 @@ export default class DomConverter { return data; } + /** + * Helper function which checks if a DOM text node, preceded by the given `prevNode` should + * be trimmed from the left side. + * + * @param {Node} prevNode + */ + _checkShouldLeftTrimDomText( prevNode ) { + if ( !prevNode ) { + return true; + } + + if ( isElement( prevNode ) ) { + return true; + } + + 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. + * + * @param {Node} node + * @param {Node} prevNode + */ + _checkShouldRightTrimDomText( node, nextNode ) { + if ( nextNode ) { + return false; + } + + return !startsWithFiller( node ); + } + /** * 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. @@ -1033,12 +1071,17 @@ export default class DomConverter { } ); for ( const value of treeWalker ) { + // ViewContainerElement is found on a way to next ViewText node, so given `node` was first/last + // text node in its container element. if ( value.item.is( 'containerElement' ) ) { - // ViewContainerElement is found on a way to next ViewText node, so given `node` was first/last - // text node in its container element. return null; - } else if ( value.item.is( 'textProxy' ) ) { - // Found a text node in the same container element. + } + //
found – it works like a block boundary, so do not scan further. + else if ( value.item.is( 'br' ) ) { + return null; + } + // Found a text node in the same container element. + else if ( value.item.is( 'textProxy' ) ) { return value.item; } } @@ -1047,15 +1090,27 @@ export default class DomConverter { } /** - * Helper function. For given `Text` node, it finds previous or next sibling that is contained in the same block element. - * If there is no such sibling, `null` is returned. + * Helper function. For the given text node, it finds the closest touching node which is either + * a text node or a `
`. The search is terminated at block element boundaries and if a matching node + * wasn't found so far, `null` is returned. + * + * In the following DOM structure: + * + *

foobar
bom

+ * + * * `foo` doesn't have its previous touching inline node (`null` is returned), + * * `foo`'s next touching inline node is `bar` + * * `bar`'s next touching inline node is `
` + * + * This method returns text nodes and `
` elements because these types of nodes affect how + * spaces in the given text node need to be converted. * * @private * @param {Text} node * @param {Boolean} getNext - * @returns {Text|null} + * @returns {Text|Element|null} */ - _getTouchingDomTextNode( node, getNext ) { + _getTouchingInlineDomNode( node, getNext ) { if ( !node.parentNode ) { return null; } @@ -1064,7 +1119,19 @@ export default class DomConverter { const document = node.ownerDocument; const topmostParent = getAncestors( node )[ 0 ]; - const treeWalker = document.createTreeWalker( topmostParent, NodeFilter.SHOW_TEXT ); + const treeWalker = document.createTreeWalker( topmostParent, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT, { + acceptNode( node ) { + if ( isText( node ) ) { + return NodeFilter.FILTER_ACCEPT; + } + + if ( node.tagName == 'BR' ) { + return NodeFilter.FILTER_ACCEPT; + } + + return NodeFilter.FILTER_SKIP; + } + } ); treeWalker.currentNode = node; diff --git a/tests/view/domconverter/dom-to-view.js b/tests/view/domconverter/dom-to-view.js index d01ec1df9..c3bf0cd76 100644 --- a/tests/view/domconverter/dom-to-view.js +++ b/tests/view/domconverter/dom-to-view.js @@ -316,6 +316,77 @@ describe( 'DomConverter', () => { expect( viewDiv.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foo' ); } ); + it( 'after a
', () => { + const domP = createElement( document, 'p', {}, [ + document.createTextNode( 'foo' ), + createElement( document, 'br' ), + document.createTextNode( ' bar' ) + ] ); + + const viewP = converter.domToView( domP ); + + expect( viewP.childCount ).to.equal( 3 ); + expect( viewP.getChild( 2 ).data ).to.equal( 'bar' ); + } ); + + it( 'after a
– two spaces', () => { + const domP = createElement( document, 'p', {}, [ + document.createTextNode( 'foo' ), + createElement( document, 'br' ), + document.createTextNode( ' \u00a0bar' ) + ] ); + + const viewP = converter.domToView( domP ); + + expect( viewP.childCount ).to.equal( 3 ); + expect( viewP.getChild( 2 ).data ).to.equal( ' bar' ); + } ); + + // This TC ensures that the algorithm stops on
. + // If not, situations like https://github.com/ckeditor/ckeditor5/issues/1024#issuecomment-393109558 might occur. + it( 'after a
– when
is preceeded with a nbsp', () => { + const domP = createElement( document, 'p', {}, [ + document.createTextNode( 'foo\u00a0' ), + createElement( document, 'br' ), + document.createTextNode( ' bar' ) + ] ); + + const viewP = converter.domToView( domP ); + + expect( viewP.childCount ).to.equal( 3 ); + expect( viewP.getChild( 2 ).data ).to.equal( 'bar' ); + } ); + + it( 'after a
– when text after that
is nested', () => { + const domP = createElement( document, 'p', {}, [ + document.createTextNode( 'foo' ), + createElement( document, 'br' ), + createElement( document, 'b', {}, [ + document.createTextNode( ' bar' ) + ] ) + ] ); + + const viewP = converter.domToView( domP ); + + expect( viewP.childCount ).to.equal( 3 ); + expect( viewP.getChild( 2 ).getChild( 0 ).data ).to.equal( 'bar' ); + } ); + + it( 'between
s - trim only the left boundary', () => { + const domP = createElement( document, 'p', {}, [ + document.createTextNode( 'x' ), + createElement( document, 'br' ), + document.createTextNode( ' foo ' ), + createElement( document, 'br' ), + document.createTextNode( 'x' ) + ] ); + + const viewP = converter.domToView( domP ); + + expect( viewP.childCount ).to.equal( 5 ); + expect( viewP.getChild( 2 ).data ).to.equal( 'foo ' ); + } ); + it( 'multiple consecutive whitespaces changed to one', () => { const domDiv = createElement( document, 'div', {}, [ createElement( document, 'p', {}, [ @@ -521,6 +592,57 @@ describe( 'DomConverter', () => { expect( viewDiv.getChild( 0 ).getChild( 1 ).getChild( 0 ).data ).to.equal( '\u00a0' ); } ); + // While we render `X 
X`, `X
X` is ok too – the space needs to be preserved. + it( 'not before a
', () => { + const domP = createElement( document, 'p', {}, [ + document.createTextNode( 'foo ' ), + createElement( document, 'br' ) + ] ); + + const viewP = converter.domToView( domP ); + + expect( viewP.childCount ).to.equal( 2 ); + expect( viewP.getChild( 0 ).data ).to.equal( 'foo ' ); + } ); + + it( 'not before a
(nbsp+space)', () => { + const domP = createElement( document, 'p', {}, [ + document.createTextNode( 'foo\u00a0 ' ), + createElement( document, 'br' ) + ] ); + + const viewP = converter.domToView( domP ); + + expect( viewP.childCount ).to.equal( 2 ); + expect( viewP.getChild( 0 ).data ).to.equal( 'foo ' ); + } ); + + it( 'before a
(space+space=>space)', () => { + const domP = createElement( document, 'p', {}, [ + document.createTextNode( 'foo ' ), + createElement( document, 'br' ) + ] ); + + const viewP = converter.domToView( domP ); + + expect( viewP.childCount ).to.equal( 2 ); + expect( viewP.getChild( 0 ).data ).to.equal( 'foo ' ); + } ); + + it( 'not before a
– when text before that
is nested', () => { + const domP = createElement( document, 'p', {}, [ + createElement( document, 'b', {}, [ + document.createTextNode( 'foo ' ) + ] ), + createElement( document, 'br' ) + ] ); + + const viewP = converter.domToView( domP ); + + expect( viewP.childCount ).to.equal( 2 ); + expect( viewP.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foo ' ); + } ); + // // See also whitespace-handling-integration.js. // diff --git a/tests/view/domconverter/view-to-dom.js b/tests/view/domconverter/view-to-dom.js index 3933ae17d..3f41b81d4 100644 --- a/tests/view/domconverter/view-to-dom.js +++ b/tests/view/domconverter/view-to-dom.js @@ -10,6 +10,7 @@ import ViewElement from '../../../src/view/element'; import ViewPosition from '../../../src/view/position'; import ViewContainerElement from '../../../src/view/containerelement'; import ViewAttributeElement from '../../../src/view/attributeelement'; +import ViewEmptyElement from '../../../src/view/emptyelement'; import DomConverter from '../../../src/view/domconverter'; import ViewDocumentFragment from '../../../src/view/documentfragment'; import { INLINE_FILLER, INLINE_FILLER_LENGTH, isBlockFiller } from '../../../src/view/filler'; @@ -248,12 +249,16 @@ describe( 'DomConverter', () => { } const domElement = converter.viewToDom( viewElement, document ); - const data = domElement.innerHTML.replace( / /g, '_' ); + const data = showNbsp( domElement.innerHTML ); expect( data ).to.equal( output ); } ); } + function showNbsp( html ) { + return html.replace( / /g, '_' ); + } + // At the beginning. test( ' x', '_x' ); test( ' x', '_ x' ); @@ -400,19 +405,213 @@ describe( 'DomConverter', () => { test( [ ' ', ' ' ], '_ _ __' ); it( 'not in preformatted blocks', () => { - const viewDiv = new ViewContainerElement( 'pre', null, [ new ViewText( ' foo ' ), new ViewText( ' bar ' ) ] ); - const domDiv = converter.viewToDom( viewDiv, document ); + const viewPre = new ViewContainerElement( 'pre', null, [ new ViewText( ' foo ' ), new ViewText( ' bar ' ) ] ); + const domPre = converter.viewToDom( viewPre, document ); - expect( domDiv.innerHTML ).to.equal( ' foo bar ' ); + expect( domPre.innerHTML ).to.equal( ' foo bar ' ); } ); - it( 'text node before in a preformatted node', () => { - const viewCode = new ViewAttributeElement( 'pre', null, new ViewText( 'foo ' ) ); - const viewDiv = new ViewContainerElement( 'div', null, [ viewCode, new ViewText( ' bar' ) ] ); + it( 'not in a preformatted block followed by a text', () => { + const viewPre = new ViewAttributeElement( 'pre', null, new ViewText( 'foo ' ) ); + const viewDiv = new ViewContainerElement( 'div', null, [ viewPre, new ViewText( ' bar' ) ] ); const domDiv = converter.viewToDom( viewDiv, document ); expect( domDiv.innerHTML ).to.equal( '
foo   
bar' ); } ); + + describe( 'around
s', () => { + it( 'before
– a single space', () => { + const viewDiv = new ViewContainerElement( 'div', null, [ + new ViewText( 'foo ' ), + new ViewEmptyElement( 'br' ), + new ViewText( 'bar' ) + ] ); + const domDiv = converter.viewToDom( viewDiv, document ); + + expect( showNbsp( domDiv.innerHTML ) ).to.equal( 'foo_
bar' ); + } ); + + it( 'before
– two spaces', () => { + const viewDiv = new ViewContainerElement( 'div', null, [ + new ViewText( 'foo ' ), + new ViewEmptyElement( 'br' ), + new ViewText( 'bar' ) + ] ); + const domDiv = converter.viewToDom( viewDiv, document ); + + expect( showNbsp( domDiv.innerHTML ) ).to.equal( 'foo _
bar' ); + } ); + + it( 'before
– three spaces', () => { + const viewDiv = new ViewContainerElement( 'div', null, [ + new ViewText( 'foo ' ), + new ViewEmptyElement( 'br' ), + new ViewText( 'bar' ) + ] ); + const domDiv = converter.viewToDom( viewDiv, document ); + + expect( showNbsp( domDiv.innerHTML ) ).to.equal( 'foo __
bar' ); + } ); + + it( 'before
– only a space', () => { + const viewDiv = new ViewContainerElement( 'div', null, [ + new ViewText( ' ' ), + new ViewEmptyElement( 'br' ), + new ViewText( 'bar' ) + ] ); + const domDiv = converter.viewToDom( viewDiv, document ); + + expect( showNbsp( domDiv.innerHTML ) ).to.equal( '_
bar' ); + } ); + + it( 'before
– only two spaces', () => { + const viewDiv = new ViewContainerElement( 'div', null, [ + new ViewText( ' ' ), + new ViewEmptyElement( 'br' ), + new ViewText( 'bar' ) + ] ); + const domDiv = converter.viewToDom( viewDiv, document ); + + expect( showNbsp( domDiv.innerHTML ) ).to.equal( '__
bar' ); + } ); + + it( 'before
– only three spaces', () => { + const viewDiv = new ViewContainerElement( 'div', null, [ + new ViewText( ' ' ), + new ViewEmptyElement( 'br' ), + new ViewText( 'bar' ) + ] ); + const domDiv = converter.viewToDom( viewDiv, document ); + + expect( showNbsp( domDiv.innerHTML ) ).to.equal( '_ _
bar' ); + } ); + + it( 'after
– a single space', () => { + const viewDiv = new ViewContainerElement( 'div', null, [ + new ViewText( 'foo' ), + new ViewEmptyElement( 'br' ), + new ViewText( ' bar' ) + ] ); + const domDiv = converter.viewToDom( viewDiv, document ); + + expect( showNbsp( domDiv.innerHTML ) ).to.equal( 'foo
_bar' ); + } ); + + it( 'after
– two spaces', () => { + const viewDiv = new ViewContainerElement( 'div', null, [ + new ViewText( 'foo' ), + new ViewEmptyElement( 'br' ), + new ViewText( ' bar' ) + ] ); + const domDiv = converter.viewToDom( viewDiv, document ); + + expect( showNbsp( domDiv.innerHTML ) ).to.equal( 'foo
_ bar' ); + } ); + + it( 'after
– three spaces', () => { + const viewDiv = new ViewContainerElement( 'div', null, [ + new ViewText( 'foo' ), + new ViewEmptyElement( 'br' ), + new ViewText( ' bar' ) + ] ); + const domDiv = converter.viewToDom( viewDiv, document ); + + expect( showNbsp( domDiv.innerHTML ) ).to.equal( 'foo
_ _bar' ); + } ); + + it( 'after
– only a space', () => { + const viewDiv = new ViewContainerElement( 'div', null, [ + new ViewText( 'foo' ), + new ViewEmptyElement( 'br' ), + new ViewText( ' ' ) + ] ); + const domDiv = converter.viewToDom( viewDiv, document ); + + expect( showNbsp( domDiv.innerHTML ) ).to.equal( 'foo
_' ); + } ); + + it( 'after
– only two spaces', () => { + const viewDiv = new ViewContainerElement( 'div', null, [ + new ViewText( 'foo' ), + new ViewEmptyElement( 'br' ), + new ViewText( ' ' ) + ] ); + const domDiv = converter.viewToDom( viewDiv, document ); + + expect( showNbsp( domDiv.innerHTML ) ).to.equal( 'foo
__' ); + } ); + + it( 'after
– only three spaces', () => { + const viewDiv = new ViewContainerElement( 'div', null, [ + new ViewText( 'foo' ), + new ViewEmptyElement( 'br' ), + new ViewText( ' ' ) + ] ); + const domDiv = converter.viewToDom( viewDiv, document ); + + expect( showNbsp( domDiv.innerHTML ) ).to.equal( 'foo
_ _' ); + } ); + + it( 'between
s – a single space', () => { + const viewDiv = new ViewContainerElement( 'div', null, [ + new ViewEmptyElement( 'br' ), + new ViewText( ' ' ), + new ViewEmptyElement( 'br' ), + new ViewText( 'foo' ) + ] ); + const domDiv = converter.viewToDom( viewDiv, document ); + + expect( showNbsp( domDiv.innerHTML ) ).to.equal( '
_
foo' ); + } ); + + it( 'between
s – only two spaces', () => { + const viewDiv = new ViewContainerElement( 'div', null, [ + new ViewEmptyElement( 'br' ), + new ViewText( ' ' ), + new ViewEmptyElement( 'br' ), + new ViewText( 'foo' ) + ] ); + const domDiv = converter.viewToDom( viewDiv, document ); + + expect( showNbsp( domDiv.innerHTML ) ).to.equal( '
__
foo' ); + } ); + + it( 'between
s – only three spaces', () => { + const viewDiv = new ViewContainerElement( 'div', null, [ + new ViewEmptyElement( 'br' ), + new ViewText( ' ' ), + new ViewEmptyElement( 'br' ), + new ViewText( 'foo' ) + ] ); + const domDiv = converter.viewToDom( viewDiv, document ); + + expect( showNbsp( domDiv.innerHTML ) ).to.equal( '
_ _
foo' ); + } ); + + it( 'between
s – space and text', () => { + const viewDiv = new ViewContainerElement( 'div', null, [ + new ViewEmptyElement( 'br' ), + new ViewText( ' foo' ), + new ViewEmptyElement( 'br' ), + new ViewText( 'foo' ) + ] ); + const domDiv = converter.viewToDom( viewDiv, document ); + + expect( showNbsp( domDiv.innerHTML ) ).to.equal( '
_foo
foo' ); + } ); + + it( 'between
s – text and space', () => { + const viewDiv = new ViewContainerElement( 'div', null, [ + new ViewEmptyElement( 'br' ), + new ViewText( 'foo ' ), + new ViewEmptyElement( 'br' ), + new ViewText( 'foo' ) + ] ); + const domDiv = converter.viewToDom( viewDiv, document ); + + expect( showNbsp( domDiv.innerHTML ) ).to.equal( '
foo_
foo' ); + } ); + } ); } ); } ); diff --git a/tests/view/domconverter/whitespace-handling-integration.js b/tests/view/domconverter/whitespace-handling-integration.js index e29ce0400..47fe30e35 100644 --- a/tests/view/domconverter/whitespace-handling-integration.js +++ b/tests/view/domconverter/whitespace-handling-integration.js @@ -5,14 +5,19 @@ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import ShiftEnter from '@ckeditor/ckeditor5-enter/src/shiftenter'; import { getData } from '../../../src/dev-utils/model'; +// NOTE: +// dev utils' setData() loses white spaces so don't use it for tests here!!! +// https://github.com/ckeditor/ckeditor5-engine/issues/1428 + describe( 'DomConverter – whitespace handling – integration', () => { let editor; // See https://github.com/ckeditor/ckeditor5-engine/issues/822. - describe( 'data loading', () => { + describe( 'normalizing whitespaces around block boundaries (#822)', () => { beforeEach( () => { return VirtualTestEditor .create( { plugins: [ Paragraph ] } ) @@ -145,4 +150,137 @@ describe( 'DomConverter – whitespace handling – integration', () => { expect( editor.getData() ).to.equal( '

foo

bar

' ); } ); } ); + + // https://github.com/ckeditor/ckeditor5/issues/1024 + describe( 'whitespaces around
s', () => { + beforeEach( () => { + return VirtualTestEditor + .create( { plugins: [ Paragraph, ShiftEnter ] } ) + .then( newEditor => { + editor = newEditor; + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + it( 'single spaces around
', () => { + editor.setData( '

foo 
 bar

' ); + + expect( getData( editor.model, { withoutSelection: true } ) ) + .to.equal( 'foo bar' ); + + expect( editor.getData() ).to.equal( '

foo 
 bar

' ); + } ); + + it( 'single spaces around
(normalization)', () => { + editor.setData( '

foo 
bar

' ); + + expect( getData( editor.model, { withoutSelection: true } ) ) + .to.equal( 'foo bar' ); + + expect( editor.getData() ).to.equal( '

foo 
bar

' ); + } ); + + it( 'two spaces before a
', () => { + editor.setData( '

foo  
bar

' ); + + expect( getData( editor.model, { withoutSelection: true } ) ) + .to.equal( 'foo bar' ); + + expect( editor.getData() ).to.equal( '

foo  
bar

' ); + } ); + + it( 'two spaces before a
(normalization)', () => { + editor.setData( '

foo 
bar

' ); + + expect( getData( editor.model, { withoutSelection: true } ) ) + .to.equal( 'foo bar' ); + + expect( editor.getData() ).to.equal( '

foo  
bar

' ); + } ); + + it( 'two spaces before a
(normalization to a model nbsp)', () => { + editor.setData( '

foo  
bar

' ); + + expect( getData( editor.model, { withoutSelection: true } ) ) + .to.equal( 'foo\u00a0 bar' ); + + expect( editor.getData() ).to.equal( '

foo  
bar

' ); + } ); + + it( 'single space after a
', () => { + editor.setData( '

foo
 bar

' ); + + expect( getData( editor.model, { withoutSelection: true } ) ) + .to.equal( 'foo bar' ); + + expect( editor.getData() ).to.equal( '

foo
 bar

' ); + } ); + + it( 'single space after a
(normalization)', () => { + editor.setData( '

foo
bar

' ); + + expect( getData( editor.model, { withoutSelection: true } ) ) + .to.equal( 'foobar' ); + + expect( editor.getData() ).to.equal( '

foo
bar

' ); + } ); + + it( 'two spaces after a
', () => { + editor.setData( '

foo
  bar

' ); + + expect( getData( editor.model, { withoutSelection: true } ) ) + .to.equal( 'foo bar' ); + + expect( editor.getData() ).to.equal( '

foo
  bar

' ); + } ); + + it( 'two spaces after a
(normalization)', () => { + editor.setData( '

foo
 bar

' ); + + expect( getData( editor.model, { withoutSelection: true } ) ) + .to.equal( 'foo bar' ); + + expect( editor.getData() ).to.equal( '

foo
 bar

' ); + } ); + + it( 'two spaces after a
(normalization to a model nbsp)', () => { + editor.setData( '

foo
  bar

' ); + + expect( getData( editor.model, { withoutSelection: true } ) ) + .to.equal( 'foo \u00a0bar' ); + + expect( editor.getData() ).to.equal( '

foo
  bar

' ); + } ); + + // https://github.com/ckeditor/ckeditor5-engine/issues/1429 + // it( 'space between
s', () => { + // editor.setData( '

foo
 
bar

' ); + + // expect( getData( editor.model, { withoutSelection: true } ) ) + // .to.equal( 'foo bar' ); + + // expect( editor.getData() ).to.equal( '

foo
 
bar

' ); + // } ); + + it( 'space between
s (normalization)', () => { + editor.setData( '

foo

bar

' ); + + expect( getData( editor.model, { withoutSelection: true } ) ) + .to.equal( 'foobar' ); + + expect( editor.getData() ).to.equal( '

foo

bar

' ); + } ); + + it( 'two spaces between
s', () => { + editor.setData( '

foo
  
bar

' ); + + expect( getData( editor.model, { withoutSelection: true } ) ) + .to.equal( 'foo bar' ); + + expect( editor.getData() ).to.equal( '

foo
  
bar

' ); + } ); + } ); } );