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

Commit 4c9a0af

Browse files
authored
Merge pull request #988 from ckeditor/t/822
Fix: Singular white spaces (new lines, tabs and carriage returns) will be ignored when loading data when used outside/between block elements. Closes #822. Also, the range of characters which are being normalized during DOM to view conversion was reduced to `[ \n\t\r]` to avoid losing space characters (which matches `/\s/`) that could be significant.
2 parents 93639d0 + 9763270 commit 4c9a0af

File tree

3 files changed

+289
-4
lines changed

3 files changed

+289
-4
lines changed

src/view/domconverter.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,10 +1037,11 @@ export default class DomConverter {
10371037
return data;
10381038
}
10391039

1040-
// Change all consecutive whitespace characters to a single space character. That's how multiple whitespaces
1041-
// are treated when rendered, so we normalize those whitespaces.
1042-
// Note that   (`\u00A0`) should not be treated as a whitespace because it is rendered.
1043-
data = data.replace( /[^\S\u00A0]{2,}/g, ' ' );
1040+
// Change all consecutive whitespace characters (from the [ \n\t\r] set –
1041+
// see https://github.com/ckeditor/ckeditor5-engine/issues/822#issuecomment-311670249) to a single space character.
1042+
// That's how multiple whitespaces are treated when rendered, so we normalize those whitespaces.
1043+
// We're replacing 1+ (and not 2+) to also normalize singular \n\t\r characters (#822).
1044+
data = data.replace( /[ \n\t\r]{1,}/g, ' ' );
10441045

10451046
const prevNode = this._getTouchingDomTextNode( node, false );
10461047
const nextNode = this._getTouchingDomTextNode( node, true );
@@ -1062,12 +1063,14 @@ export default class DomConverter {
10621063
// ` \u00A0` to ensure proper rendering. Since here we convert back, we recognize those pairs and change them
10631064
// to ` ` which is what we expect to have in model/view.
10641065
data = data.replace( / \u00A0/g, ' ' );
1066+
10651067
// Then, change   character that is at the beginning of the text node to space character.
10661068
// As above, that   was created for rendering reasons but it's real meaning is just a space character.
10671069
// We do that replacement only if this is the first node or the previous node ends on whitespace character.
10681070
if ( !prevNode || /[^\S\u00A0]/.test( prevNode.data.charAt( prevNode.data.length - 1 ) ) ) {
10691071
data = data.replace( /^\u00A0/, ' ' );
10701072
}
1073+
10711074
// Since input text data could be: `x_ _`, we would not replace the first   after `x` character.
10721075
// We have to fix it. Since we already change all `  `, we will have something like this at the end of text data:
10731076
// `x_ _ _` -> `x_ `. Find   at the end of string (can be followed only by spaces).

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

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,111 @@ describe( 'DomConverter', () => {
212212
expect( viewDiv.getChild( 1 ).getChild( 0 ).data ).to.equal( 'foo' );
213213
} );
214214

215+
it( 'after a block element', () => {
216+
const domDiv = createElement( document, 'div', {}, [
217+
createElement( document, 'p', {}, [
218+
document.createTextNode( 'foo' )
219+
] ),
220+
document.createTextNode( ' ' )
221+
] );
222+
223+
const viewDiv = converter.domToView( domDiv );
224+
225+
expect( viewDiv.childCount ).to.equal( 1 );
226+
expect( viewDiv.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foo' );
227+
} );
228+
229+
it( 'after a block element (new line)', () => {
230+
const domDiv = createElement( document, 'div', {}, [
231+
createElement( document, 'p', {}, [
232+
document.createTextNode( 'foo' )
233+
] ),
234+
document.createTextNode( '\n' )
235+
] );
236+
237+
const viewDiv = converter.domToView( domDiv );
238+
239+
expect( viewDiv.childCount ).to.equal( 1 );
240+
expect( viewDiv.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foo' );
241+
} );
242+
243+
it( 'after a block element (carriage return)', () => {
244+
const domDiv = createElement( document, 'div', {}, [
245+
createElement( document, 'p', {}, [
246+
document.createTextNode( 'foo' )
247+
] ),
248+
document.createTextNode( '\r' )
249+
] );
250+
251+
const viewDiv = converter.domToView( domDiv );
252+
253+
expect( viewDiv.childCount ).to.equal( 1 );
254+
expect( viewDiv.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foo' );
255+
} );
256+
257+
it( 'after a block element (tab)', () => {
258+
const domDiv = createElement( document, 'div', {}, [
259+
createElement( document, 'p', {}, [
260+
document.createTextNode( 'foo' )
261+
] ),
262+
document.createTextNode( '\t' )
263+
] );
264+
265+
const viewDiv = converter.domToView( domDiv );
266+
267+
expect( viewDiv.childCount ).to.equal( 1 );
268+
expect( viewDiv.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foo' );
269+
} );
270+
271+
// See https://github.com/ckeditor/ckeditor5-engine/issues/822#issuecomment-311670249
272+
it( 'but preserve all except " \\n\\r\\t"', () => {
273+
const domDiv = createElement( document, 'div', {}, [
274+
createElement( document, 'p', {}, [
275+
document.createTextNode( 'x\fx\vx\u00a0x\u1680x\u2000x\u200ax\u2028x\u2029x\u202fx\u205fx\u3000x\ufeffx' )
276+
] ),
277+
createElement( document, 'p', {}, [
278+
// x<two spaces>x because it behaved differently than "x<space>x" when I've been fixing this
279+
document.createTextNode( 'x\f\vx\u00a0\u1680x\u2000\u200ax\u2028\u2029x\u202f\u205fx\u3000\ufeffx' )
280+
] )
281+
] );
282+
283+
const viewDiv = converter.domToView( domDiv );
284+
285+
expect( viewDiv.childCount ).to.equal( 2 );
286+
expect( viewDiv.getChild( 0 ).getChild( 0 ).data )
287+
.to.equal( 'x\fx\vx\u00a0x\u1680x\u2000x\u200ax\u2028x\u2029x\u202fx\u205fx\u3000x\ufeffx' );
288+
expect( viewDiv.getChild( 1 ).getChild( 0 ).data )
289+
.to.equal( 'x\f\vx\u00a0\u1680x\u2000\u200ax\u2028\u2029x\u202f\u205fx\u3000\ufeffx' );
290+
} );
291+
292+
it( 'before a block element', () => {
293+
const domDiv = createElement( document, 'div', {}, [
294+
document.createTextNode( ' ' ),
295+
createElement( document, 'p', {}, [
296+
document.createTextNode( ' foo' )
297+
] )
298+
] );
299+
300+
const viewDiv = converter.domToView( domDiv );
301+
302+
expect( viewDiv.childCount ).to.equal( 1 );
303+
expect( viewDiv.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foo' );
304+
} );
305+
306+
it( 'before a block element (new line)', () => {
307+
const domDiv = createElement( document, 'div', {}, [
308+
document.createTextNode( '\n' ),
309+
createElement( document, 'p', {}, [
310+
document.createTextNode( 'foo' )
311+
] )
312+
] );
313+
314+
const viewDiv = converter.domToView( domDiv );
315+
316+
expect( viewDiv.childCount ).to.equal( 1 );
317+
expect( viewDiv.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foo' );
318+
} );
319+
215320
it( 'multiple consecutive whitespaces changed to one', () => {
216321
const domDiv = createElement( document, 'div', {}, [
217322
createElement( document, 'p', {}, [
@@ -228,6 +333,21 @@ describe( 'DomConverter', () => {
228333
expect( viewDiv.getChild( 1 ).getChild( 0 ).data ).to.equal( 'fo o' );
229334
} );
230335

336+
it( 'multiple consecutive whitespaces changed to one (tab, new line, carriage return)', () => {
337+
const domDiv = createElement( document, 'div', {}, [
338+
document.createTextNode( '\n\n \t\r\n' ),
339+
createElement( document, 'p', {}, [
340+
document.createTextNode( 'f\n\t\r\n\to\n\n\no' )
341+
] ),
342+
document.createTextNode( '\n\n \t\r\n' )
343+
] );
344+
345+
const viewDiv = converter.domToView( domDiv );
346+
347+
expect( viewDiv.childCount ).to.equal( 1 );
348+
expect( viewDiv.getChild( 0 ).getChild( 0 ).data ).to.equal( 'f o o' );
349+
} );
350+
231351
function test( inputTexts, output ) {
232352
if ( typeof inputTexts == 'string' ) {
233353
inputTexts = [ inputTexts ];
@@ -339,6 +459,10 @@ describe( 'DomConverter', () => {
339459

340460
expect( viewDiv.getChild( 0 ).getChild( 0 ).data ).to.equal( ' foo\n foo ' );
341461
} );
462+
463+
//
464+
// See also whitespace-handling-integration.js.
465+
//
342466
} );
343467
} );
344468

@@ -602,6 +726,8 @@ describe( 'DomConverter', () => {
602726

603727
expect( viewSelection.rangeCount ).to.equal( 1 );
604728
expect( stringify( viewP, viewSelection.getFirstRange() ) ).to.equal( '<p>f{oo<b>ba}r</b></p>' );
729+
730+
domP.remove();
605731
} );
606732

607733
it( 'should convert empty selection to empty selection', () => {
@@ -638,6 +764,8 @@ describe( 'DomConverter', () => {
638764
expect( viewSelection.anchor.offset ).to.equal( 2 );
639765
expect( viewSelection.focus.offset ).to.equal( 1 );
640766
expect( viewSelection.isBackward ).to.be.true;
767+
768+
domP.remove();
641769
} );
642770

643771
it( 'should not add null ranges', () => {
@@ -659,6 +787,8 @@ describe( 'DomConverter', () => {
659787
const viewSelection = converter.domSelectionToView( domSelection );
660788

661789
expect( viewSelection.rangeCount ).to.equal( 0 );
790+
791+
domP.remove();
662792
} );
663793

664794
it( 'should return fake selection', () => {
@@ -679,6 +809,8 @@ describe( 'DomConverter', () => {
679809
const bindViewSelection = converter.domSelectionToView( domSelection );
680810

681811
expect( bindViewSelection.isEqual( viewSelection ) ).to.be.true;
812+
813+
domContainer.remove();
682814
} );
683815

684816
it( 'should return fake selection if selection is placed inside text node', () => {
@@ -699,6 +831,8 @@ describe( 'DomConverter', () => {
699831
const bindViewSelection = converter.domSelectionToView( domSelection );
700832

701833
expect( bindViewSelection.isEqual( viewSelection ) ).to.be.true;
834+
835+
domContainer.remove();
702836
} );
703837
} );
704838
} );
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/**
2+
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3+
* For licensing, see LICENSE.md.
4+
*/
5+
6+
import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
7+
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
8+
9+
import { getData } from '../../../src/dev-utils/model';
10+
11+
describe( 'DomConverter – whitespace handling – integration', () => {
12+
let editor;
13+
14+
// See https://github.com/ckeditor/ckeditor5-engine/issues/822.
15+
describe( 'data loading', () => {
16+
beforeEach( () => {
17+
return VirtualTestEditor
18+
.create( { plugins: [ Paragraph ] } )
19+
.then( newEditor => {
20+
editor = newEditor;
21+
} );
22+
} );
23+
24+
afterEach( () => {
25+
return editor.destroy();
26+
} );
27+
28+
it( 'new line at the end of the content is ignored', () => {
29+
editor.setData( '<p>foo</p>\n' );
30+
31+
expect( getData( editor.document, { withoutSelection: true } ) )
32+
.to.equal( '<paragraph>foo</paragraph>' );
33+
34+
expect( editor.getData() ).to.equal( '<p>foo</p>' );
35+
} );
36+
37+
it( 'whitespaces at the end of the content are ignored', () => {
38+
editor.setData( '<p>foo</p>\n\r\n \t' );
39+
40+
expect( getData( editor.document, { withoutSelection: true } ) )
41+
.to.equal( '<paragraph>foo</paragraph>' );
42+
43+
expect( editor.getData() ).to.equal( '<p>foo</p>' );
44+
} );
45+
46+
// Controversial result. See https://github.com/ckeditor/ckeditor5-engine/issues/987.
47+
it( 'nbsp at the end of the content is not ignored', () => {
48+
editor.setData( '<p>foo</p>' );
49+
50+
expect( getData( editor.document, { withoutSelection: true } ) )
51+
.to.equal( '<paragraph>foo</paragraph>' );
52+
53+
expect( editor.getData() ).to.equal( '<p>foo</p>' );
54+
} );
55+
56+
it( 'new line at the beginning of the content is ignored', () => {
57+
editor.setData( '\n<p>foo</p>' );
58+
59+
expect( getData( editor.document, { withoutSelection: true } ) )
60+
.to.equal( '<paragraph>foo</paragraph>' );
61+
62+
expect( editor.getData() ).to.equal( '<p>foo</p>' );
63+
} );
64+
65+
it( 'whitespaces at the beginning of the content are ignored', () => {
66+
editor.setData( '\n\n \t<p>foo</p>' );
67+
68+
expect( getData( editor.document, { withoutSelection: true } ) )
69+
.to.equal( '<paragraph>foo</paragraph>' );
70+
71+
expect( editor.getData() ).to.equal( '<p>foo</p>' );
72+
} );
73+
74+
// Controversial result. See https://github.com/ckeditor/ckeditor5-engine/issues/987.
75+
it( 'nbsp at the beginning of the content is not ignored', () => {
76+
editor.setData( '<p>foo</p>' );
77+
78+
expect( getData( editor.document, { withoutSelection: true } ) )
79+
.to.equal( '<paragraph>foo</paragraph>' );
80+
81+
expect( editor.getData() ).to.equal( '<p>foo</p>' );
82+
} );
83+
84+
it( 'new line between blocks is ignored', () => {
85+
editor.setData( '<p>foo</p>\n<p>bar</p>' );
86+
87+
expect( getData( editor.document, { withoutSelection: true } ) )
88+
.to.equal( '<paragraph>foo</paragraph><paragraph>bar</paragraph>' );
89+
90+
expect( editor.getData() ).to.equal( '<p>foo</p><p>bar</p>' );
91+
} );
92+
93+
it( 'whitespaces between blocks are ignored', () => {
94+
editor.setData( '<p>foo</p>\n\n \t<p>bar</p>' );
95+
96+
expect( getData( editor.document, { withoutSelection: true } ) )
97+
.to.equal( '<paragraph>foo</paragraph><paragraph>bar</paragraph>' );
98+
99+
expect( editor.getData() ).to.equal( '<p>foo</p><p>bar</p>' );
100+
} );
101+
102+
// Controversial result. See https://github.com/ckeditor/ckeditor5-engine/issues/987.
103+
it( 'nbsp between blocks is not ignored', () => {
104+
editor.setData( '<p>foo</p>&nbsp;<p>bar</p>' );
105+
106+
expect( getData( editor.document, { withoutSelection: true } ) )
107+
.to.equal( '<paragraph>foo</paragraph><paragraph>bar</paragraph>' );
108+
109+
expect( editor.getData() ).to.equal( '<p>foo</p><p>bar</p>' );
110+
} );
111+
112+
it( 'new lines inside blocks are ignored', () => {
113+
editor.setData( '<p>\nfoo\n</p>' );
114+
115+
expect( getData( editor.document, { withoutSelection: true } ) )
116+
.to.equal( '<paragraph>foo</paragraph>' );
117+
118+
expect( editor.getData() ).to.equal( '<p>foo</p>' );
119+
} );
120+
121+
it( 'whitespaces inside blocks are ignored', () => {
122+
editor.setData( '<p>\n\n \tfoo\n\n \t</p>' );
123+
124+
expect( getData( editor.document, { withoutSelection: true } ) )
125+
.to.equal( '<paragraph>foo</paragraph>' );
126+
127+
expect( editor.getData() ).to.equal( '<p>foo</p>' );
128+
} );
129+
130+
it( 'nbsp inside blocks are not ignored', () => {
131+
editor.setData( '<p>&nbsp;foo&nbsp;</p>' );
132+
133+
expect( getData( editor.document, { withoutSelection: true } ) )
134+
.to.equal( '<paragraph> foo </paragraph>' );
135+
136+
expect( editor.getData() ).to.equal( '<p>&nbsp;foo&nbsp;</p>' );
137+
} );
138+
139+
it( 'all whitespaces together are ignored', () => {
140+
editor.setData( '\n<p>foo\n\r\n \t</p>\n<p> bar</p>' );
141+
142+
expect( getData( editor.document, { withoutSelection: true } ) )
143+
.to.equal( '<paragraph>foo</paragraph><paragraph>bar</paragraph>' );
144+
145+
expect( editor.getData() ).to.equal( '<p>foo</p><p>bar</p>' );
146+
} );
147+
} );
148+
} );

0 commit comments

Comments
 (0)